DataTable

A table with sorting, row selection, and column visibility, all driven by state you own.

Import

ts
import { DataTable, type DataTableSort } from '@k8ordo/ui';

Usage

Every piece of state is yours (controlled). DataTable takes the sort state but does not sort — it draws rows in the order given, so the same component works when the server sorts. Each feature appears only when you pass the handler that receives its changes.

Role
AokiDesigner3
InoueEngineer8
UedaEngineer5
tsx
const [sort, setSort] = useState<DataTableSort | null>(null);
const [selectedIds, setSelectedIds] = useState<string[]>([]);
const [hiddenColumnIds, setHiddenColumnIds] = useState<string[]>([]);

<DataTable
  columns={[
    { id: 'name', header: 'Name', cell: (m) => m.name, sortable: true },
    { id: 'role', header: 'Role', cell: (m) => <Badge label={m.role} /> },
    {
      id: 'projects',
      header: 'Projects',
      cell: (m) => m.projects,
      align: 'right',
      sortable: true,
    },
  ]}
  getRowId={(m) => m.id}
  hiddenColumnIds={hiddenColumnIds}
  label="Members"
  onHiddenColumnIdsChange={setHiddenColumnIds}
  onSelectedIdsChange={setSelectedIds}
  onSortChange={setSort}
  rows={sortMembers(members, sort)}
  selectedIds={selectedIds}
  sort={sort}
/>

Keeping the Sort and the Page in the URL

This example keeps the sort and the page in @k8ordo/state’s url slot. Sort, and the address bar changes: whoever you send the link to sees the same order on the same page, and Back returns to the previous order.

Role
AokiDesigner3
InoueEngineer8
UedaEngineer5
tsx
// members-state.ts
export const membersState = definePageState('members', {
  url: z.object({
    sort: z._default(z.enum(['', 'name', 'projects']), ''),
    dir: z._default(z.enum(['ascending', 'descending']), 'ascending'),
    page: z._default(z.coerce.number().check(z.int(), z.gte(1)), 1),
  }),
});

// members-table.tsx
const [{ sort, dir, page }, update] = useAppState(membersState);
const current = sort === '' ? null : { columnId: sort, direction: dir };
const rows = sortMembers(members, current);

<DataTable
  columns={columns}
  getRowId={(m) => m.id}
  label="Members"
  onSortChange={(next) => {
    update(
      next === null
        ? { sort: '', dir: 'ascending', page: 1 }
        : { sort: next.columnId, dir: next.direction, page: 1 },
      { history: 'push' },
    );
  }}
  rows={rows.slice((page - 1) * PAGE_SIZE, page * PAGE_SIZE)}
  sort={current}
/>
<Pagination
  currentPage={page}
  onChange={(next) => update({ page: next }, { history: 'push' })}
  totalPages={Math.ceil(rows.length / PAGE_SIZE)}
/>

No Rows

When rows is empty, emptyState is drawn in a row spanning the columns. Pass an EmptyState.

NameRoleProjects

No members yet

tsx
<DataTable
  columns={columns}
  emptyState={<EmptyState title="No members yet" />}
  getRowId={(m) => m.id}
  label="Members"
  rows={[]}
/>

Props

columns
Type: ReadonlyArray<DataTableColumn<Row>>
Default: -
getRowId
Type: (row: Row) => string
Default: -
label
Type: string
Default: -
rows
Type: readonly Row[]
Default: -
emptyState?
Type: ReactNode
Default: -
hiddenColumnIds?
Type: readonly string[]
Default: NONE
onHiddenColumnIdsChange?
Type: (ids: string[]) => void
Default: -
onSelectedIdsChange?
Type: (ids: string[]) => void
Default: -
onSortChange?
Type: (sort: DataTableSort | null) => void
Default: -
selectedIds?
Type: readonly string[]
Default: NONE
sort?
Type: DataTableSort|null
Default: null

Wording this component renders (labels, placeholders, and the like) comes from the message dictionary when no prop sets it. To change it, see: i18n