DEV Community

Cover image for Build a sortable, selectable data table
omkar sahu
omkar sahu

Posted on Originally published at oks-ui.com on

Build a sortable, selectable data table

Search, sort, select, paginate — almost every admin screen needs the same data table, and almost every team ends up hand-rolling it because no single component quite covers all four. Here's the whole thing with Table, Pagination and FormFieldSet, wired together with plain React state.

1. The shape of the data

Table takes rows as plain objects and a getRowKey function — no adapter layer, no special row type.

type Row = { id: number; name: string; email: string; role: string };

const DATA: Row[] = [
  { id: 1, name: "Ada Lovelace", email: "ada@oks-ui.com", role: "Owner" },
  { id: 2, name: "Grace Hopper", email: "grace@oks-ui.com", role: "Admin" },
  { id: 3, name: "Alan Turing", email: "alan@oks-ui.com", role: "Member" },
  // ...
];
Enter fullscreen mode Exit fullscreen mode

2. Search filters the rows, not the table

Table itself has no search box — it just renders whatever rows you give it. Filtering lives in your own state, one useMemo:

const [query, setQuery] = useState("");
const [page, setPage] = useState(1);

const filtered = useMemo(
  () => DATA.filter((r) => r.name.toLowerCase().includes(query.toLowerCase())),
  [query],
);
Enter fullscreen mode Exit fullscreen mode

3. Sorting, selection, and the table itself

Pass sortable: true on a column and Table handles the click-to-sort interface; omit onSortChange and it sorts client-side automatically — supply it and you get the descriptor back instead, for a server-side sort. selectionMode="multiple" plus selectedKeys and onSelectionChange gets you checkboxes and an indeterminate header state for free:

const [selected, setSelected] = useState<Set<number>>(new Set());

<Table
  aria-label="Team members"
  columns={[
    { key: "name", header: "Name", sortable: true },
    { key: "email", header: "Email" },
    { key: "role", header: "Role" },
  ]}
  rows={pageRows}
  getRowKey={(r) => r.id}
  selectionMode="multiple"
  selectedKeys={selected}
  onSelectionChange={(keys) => setSelected(keys as Set<number>)}
/>
Enter fullscreen mode Exit fullscreen mode

aria-label is required, not optional. Table throws in development if you skip it — a data table with no accessible name is unusable with a screen reader, so the component won't let you forget it.

4. Pagination, wired to the same filtered list

Pagination and PaginationSummary are just told the current page, the page count and the total — they don't know or care that the underlying data came from a search filter:

const PAGE_SIZE = 4;
const pageRows = filtered.slice((page - 1) * PAGE_SIZE, page * PAGE_SIZE);

<PaginationSummary page={page} pageSize={PAGE_SIZE} total={filtered.length} />
<Pagination
  page={page}
  pageCount={Math.max(1, Math.ceil(filtered.length / PAGE_SIZE))}
  onChange={setPage}
/>
Enter fullscreen mode Exit fullscreen mode

Loading, empty and expanded states

Three props round out the states a real table needs: isLoading swaps in skeleton rows, emptyContent replaces the body when rows is empty (it already defaults to a sensible empty state), and renderExpandedRow adds a chevron-toggle column for a details panel under any row — no layout work required for any of the three.
Full prop reference at the Table component page, and this exact example — search, sort, select, paginate, wired end to end — as a live preview with the complete source at the data table pattern.


Originally published at oks-ui.com.

Top comments (0)