Recipes
Short answers to the questions that come up once a table is real: saved views, row navigation, custom sort orders, bulk actions and more.
Each of these is a few lines. They are collected here because they are the questions that arrive after the table is working, not before.
A row that navigates instead of selecting
Cancel the default by not calling next:
handlers={{ onRowClick: ({ row }) => navigate('/people/' + row.id) }}
The row still gets focus and keyboard activation; only the selection behaviour is replaced. See Handler middleware.
A domain sort order
Status columns rarely sort alphabetically. Register a comparator and name it:
type Severity = 'critical' | 'high' | 'normal' | 'low';
const RANK: Record<Severity, number> = { critical: 0, high: 1, normal: 2, low: 3 };
<DataTable
sortingFns={{
severity: (a, b, id) => RANK[a.getValue<Severity>(id)] - RANK[b.getValue<Severity>(id)],
}}
columns={[col.accessor('severity', { sortingFn: 'severity' })]}
/>;
Saved views
A view is a state object. Store it and apply it:
const view = table.getState(); // save
table.setState(savedView); // restore
For the reader's own layout (visible columns, order, widths, density), use
syncState.storage instead and it happens automatically.
A bulk action over everything matching
Send the query, not a list of ids; there may be a hundred thousand of them:
renderBulkActions={({ selectionQuery, selectedRows }) => (
<button onClick={() => api.archive(selectionQuery)}>
Archive {selectedRows.length}
</button>
)}
See Row selection.
An empty state that knows why it is empty
renderEmptyState={({ table }) =>
table.getActiveFilterCount() > 0 ? (
<>
<p>No people match these filters.</p>
<button onClick={() => table.clearAllFilters()}>Clear filters</button>
</>
) : (
<p>No people yet.</p>
)
}
A refresh button
renderToolbarEnd={(table) => <button onClick={() => table.refresh()}>Refresh</button>}
refresh() refetches with the current query; invalidate() also drops cached facets and lazily
loaded children.
A column that exports differently from how it displays
col.accessor('total', {
format: (v) => money.format(v),
exportValue: (row) => row.total,
});
The reader sees $1,240, the spreadsheet gets 1240. See Export.
Two tables sharing one filter
Lift the state. Both tables are controlled from the same value:
const [globalFilter, setGlobalFilter] = useState('');
<DataTable state={{ globalFilter }} onGlobalFilterChange={setGlobalFilter} />
<DataTable state={{ globalFilter }} onGlobalFilterChange={setGlobalFilter} />
Prefetching a detail panel on hover
slotProps={{ row: ({ row }) => ({ onMouseEnter: () => prefetch(row.id) }) }}
Handlers chain, so this does not replace the table's own mouse handling.
Related
- Handler middleware: the cancellation model.
- Slots:
slotProps,classNamesandstyles. - Persistence: saving state properly.
- Best practices: the longer-form advice.