Skip to content

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.