Shadcn React Table

Search documentation

Search the docs

State management

The data table keeps all of its state in TanStack Table's store: the standard table state (sorting, filters, selection…) and its own UI state (density, editing, filter modes…). Every slice can be left uncontrolled, observed, or fully controlled, and parts of the table re-render only for the slices they show.

TanStack table state

All the standard table state — sorting, column filters, global filter, pagination, row selection, column visibility, ordering, pinning, sizing, expansion, grouping — is plain TanStack Table state. useDataTable spreads its options straight into TanStack's useTable, so you control it exactly as you would in TanStack: pass state + the matching onXChange setter (and optionally initialState for uncontrolled seeds).

tsx
const [sorting, setSorting] = React.useState<SortingState>([])
const [rowSelection, setRowSelection] = React.useState({})

const table = useDataTable({
  data,
  columns,
  getRowId: (row) => row.id,
  state: { sorting, rowSelection },
  onSortingChange: setSorting,
  onRowSelectionChange: setRowSelection,
})

For server-side data, combine this with the manual* flags — see Server-side data.

Data table UI state

The state the table adds on top of TanStack is table state too: density, full screen, filter-row visibility, whether the search box is open, the global search mode, column filter modes, the advanced filter and its dialog, and inline editing (the cell or row being edited, the create form, the draft). It sits in table.getState() next to sorting, takes initialState and controlled state like any slice, and reports changes through on…Change callbacks with the new value.

tsx
const [editingCell, setEditingCell] = React.useState<EditingCell | null>(null)

const table = useDataTable({
  data,
  columns,
  getRowId: (row) => row.id,
  initialState: { density: "compact", showColumnFilters: true },
  state: { editingCell },
  onEditingCellChange: setEditingCell,
})

Observe a slice without taking it over with its on…Change callback:

tsx
const table = useDataTable({
  data,
  columns,
  getRowId: (row) => row.id,
  initialState: { density: "compact" },
  onDensityChange: (density) => saveToLocalStorage("density", density),
})
PropTypeDescription
onDensityChange?(density: Density) => voidCalled whenever the density changes (toolbar toggle or programmatic). Set it with `initialState.density`, control it with `state.density`.
onIsFullscreenChange?(isFullscreen: boolean) => voidCalled whenever the full-screen state is toggled. Control it with `state.isFullscreen`.
onShowColumnFiltersChange?(showColumnFilters: boolean) => voidCalled whenever the filter row is shown or hidden. Set it with `initialState.showColumnFilters`, control it with `state.showColumnFilters`.
onGlobalFilterModeChange?(mode: GlobalFilterMode) => voidCalled whenever the global search mode changes (default `"fuzzy"`). Set it with `initialState.globalFilterMode`, control it with `state.globalFilterMode`.
onColumnFilterModesChange?(modes: Record<string, FilterMode>) => voidCalled when a column's filter mode changes, with every column's mode. Control it with `state.columnFilterModes`.
onShowAdvancedFilterPanelChange?(open: boolean) => voidCalled when the advanced filter dialog opens or closes. Control it with `state.showAdvancedFilterPanel`.
onEditingCellChange?(cell: EditingCell | null) => voidCalled when cell editing starts or ends (`null`). Control it with `state.editingCell`.
onEditingRowIdChange?(rowId: string | null) => voidCalled when row editing starts or ends (`null`). Control it with `state.editingRowId`.
onIsCreatingChange?(isCreating: boolean) => voidCalled when the new-row editor opens or closes. Control it with `state.isCreating`.
onRowDraftChange?(draft: Record<string, unknown>) => voidCalled when the draft of the row being edited or created changes. Control it with `state.rowDraft`.
Controlled stateSorting and density controlled from outside the table.
GitHub
{"sorting":[{"id":"salary","desc":true}],"density":"compact"}
ID
NoahSilvaEditorDesignactive28$134,754Apr 8, 2024
75%
LiamNguyenAdminMarketingpending25$132,617Mar 28, 2024
66%
AvaThompsonOwnerEngineeringinactive22$130,480Mar 17, 2024
57%
MasonTanakaViewerSupportactive59$128,343Mar 6, 2024
48%
EllaKhanEditorSalespending56$126,206Feb 24, 2024
39%
LeoHaddadAdminDesigninactive53$124,069Feb 13, 2024
30%
HarperDuboisOwnerMarketingactive50$121,932Feb 2, 2024
21%
JackDiazViewerEngineeringpending47$119,795Jan 22, 2024
12%
AmeliaNovakEditorSupportinactive44$117,658Jan 11, 2024
3%
HenryCohenAdminSalesactive41$115,521Dec 31, 2023
95%
$4,120,536
Rows per page
1–10 of 48

Reading state in your component

<DataTable> subscribes to the state it shows. The component that calls useDataTable does not re-render when the table's state changes, so code there that reads the state during render would show an old value. Tell useDataTable what your render reads with stateSelector; the component then re-renders when that part changes (compared shallowly):

tsx
const table = useDataTable({
  data,
  columns,
  getRowId: (row) => row.id,
  enableRowSelection: true,
  stateSelector: (state) => state.rowSelection,
})

const selectedCount = Object.keys(table.getState().rowSelection).length

To re-render only a small part of your component, use table.Subscribe instead:

tsx
<table.Subscribe selector={(state) => state.rowSelection}>
  {(rowSelection) => <span>{Object.keys(rowSelection).length} selected</span>}
</table.Subscribe>

Event handlers, effects and the table's own render props (renderTopToolbarCustomActions, renderBottomToolbarCustomActions, …) always see the current state; only reads during your component's render need this.

Reading and setting from the instance

Whether controlled or not, the current values are in table.getState() and the setters are methods on table:

tsx
table.getState().density // "comfortable" | "compact" | "spacious"
table.setDensity("compact")
table.setIsFullscreen(true)
table.setShowColumnFilters((prev) => !prev)

Persisting state

Mirror any of these into local storage or a query param, then restore via the matching default* (uncontrolled) or controlled value on the next load.

Styling without props

The table is styled with Tailwind, not per-component style props. Structural slots emit a data-slot attribute you can target with a selector — see the table in Toolbar customization.