Data tables are everywhere and deceptively deep: a column model, sorting/filtering/pagination, scaling to large datasets, and proper table semantics. The pivotal decision is where the work happens — the client or the server. We'll use the RADIO framework.
The single most important question is how much data there is — it decides whether sort/filter/paginate happen on the client or the server.
Before designing, what should you nail down?
Functional vs non-functional
Functional: render rows/columns, sort, filter, paginate, maybe select/edit. Non-functional: stays responsive at the required data scale, uses semantic table markup with sort state announced, and the layout works on small screens.
Drive everything from a column model (data describing each column). A hook holds sort/filter/page state and produces the visible rows; the table, header, and rows are presentational.
Click a node to see what it owns.
Does: Composition root; renders from the column model.
State: none (delegates to useTable)
Does: Owns sort/filter/page state; computes the visible rows (or requests them from the server).
State: sort, filters, page, pageSize, selection
Does: Sortable column headers with aria-sort.
Does: Renders visible rows (windowed for large data).
Does: Presentational; cell content via the column accessor.
Does: Page controls / 'load more'.
Where do sort / filter / paginate happen?
Pick when: Large datasets (tens of thousands+). The server sorts/filters/pages; the client renders a page.
The column model makes the table generic: each column says how to render and sort itself, so the table component never hard-codes fields.
Column model + table state.
Hand-roll the table logic or use a headless library?
Pick when: Non-trivial tables — sorting, filtering, grouping, virtualization, column sizing without reinventing them.
Take columns + data and emit change events; controlled state lets the parent drive server-side operations.
Component contract.
The concerns that matter: scaling (client vs server + virtualization), accessibility, and rendering performance.
The signature insight: scale decides the architecture
For a few hundred rows, do everything on the client — instant and simple (the demo). For large datasets, the browser can't download or sort millions of rows: push sort/filter/pagination to the server and virtualize the rendered rows so the DOM stays small. Picking client-side for big data (or server-side for tiny data) is the classic mistake — match the approach to the row count.
Sortable, filterable, paginated table (editable)
All client-side here. Click a header to sort (note aria-sort), filter by name/role, and page through. Built on a semantic <table>.
The rest:
<table>/<th scope>/<td>, set aria-sort on the active column header, add a <caption>, and keep sort controls keyboard-operable. Don't rebuild a table out of <div>s and lose semantics.Self-check: a production-ready table covers…
0 / 8 covered
Going deeper (tap to reveal)