| name | table-state |
| description | Read, select, subscribe to, and control React Table V9 state with useTable selectors, table.state, table.Subscribe, table.atoms, table.store, and external TanStack Store atoms. Load for controlled state, render performance, or React Compiler builder-method subscription problems.
|
| metadata | {"type":"framework","library":"@tanstack/react-table","library_version":"9.2.4","framework":"react"} |
| requires | ["@tanstack/table-core#core","getting-started"] |
| sources | ["TanStack/table:docs/framework/react/guide/table-state.md","TanStack/table:docs/framework/react/guide/react-compiler.md","TanStack/table:examples/react/basic-subscribe","TanStack/table:packages/react-table/src/Subscribe.ts","TanStack/table:packages/react-table/src/useTable.ts"] |
This skill builds on @tanstack/table-core#core and getting-started. Read them first for table construction and feature-owned state.
State Mental Model
TanStack Table is primarily a state coordinator. Keep state internal unless another subsystem needs to read, persist, validate, or drive it. With no initialState, atoms, state, or on[State]Change options, the table owns all registered slices.
table.baseAtoms are the internal writable atoms initialized from resolved initial state.
table.atoms are readonly derived atoms for the active owner of each registered slice.
table.store combines those atoms into one readonly flat store.
table.state is only the value selected by the second useTable argument.
State is feature-based. Registering rowPaginationFeature creates pagination state and APIs; without it, pagination must not exist in initialState, state, atoms, table.atoms, table.store, or table.state. Treat a missing state API as a likely missing feature import, not a typing problem.
Keep features, data, and columns stable. State subscriptions do not compensate for new model-input references on every render.
Setup
import {
rowSelectionFeature,
tableFeatures,
useTable,
} from '@tanstack/react-table'
const features = tableFeatures({ rowSelectionFeature })
export function SelectionCount({
data,
columns,
}: {
data: Array<{ id: string }>
columns: any[]
}) {
const table = useTable({ features, data, columns }, (state) => ({
rowSelection: state.rowSelection,
}))
return <output>{Object.keys(table.state.rowSelection).length}</output>
}
The optional selector controls which state changes rerender the component and which selected fields appear on table.state. Omitting it selects all registered slices.
Core Patterns
Subscribe at the expensive boundary
function SelectedRows({
table,
}: {
table: ReturnType<typeof useTable<typeof features, { id: string }>>
}) {
return (
<table.Subscribe selector={(state) => state.rowSelection}>
{(rowSelection) => <output>{Object.keys(rowSelection).length}</output>}
</table.Subscribe>
)
}
At a top-level component holding the adapter's table instance, table.Subscribe selects from table.store. Use this after measuring or when the React Compiler cannot see state reads hidden behind table builder methods.
Control a slice with an external atom
import { useCreateAtom } from '@tanstack/react-store'
const selection = useCreateAtom<Record<string, boolean>>({})
const table = useTable({
features,
columns,
data,
atoms: { rowSelection: selection },
})
An external atom is both ownership and subscription source; it avoids value-or-updater glue.
Control a slice with React state
const [rowSelection, setRowSelection] = useState<Record<string, boolean>>({})
const table = useTable({
features,
columns,
data,
state: { rowSelection },
onRowSelectionChange: setRowSelection,
})
Choose State Ownership
Choose exactly one owner for each slice:
- Use internal state by default and call feature APIs such as
table.setSorting, table.nextPage, column.toggleVisibility, or row.toggleSelected.
- Use
initialState.<slice> only to set the starting and reset value. Changing initialState later does not reset the table.
- Prefer a stable external atom in
atoms.<slice> when Table, Query, routing, or another component must share the slice. Table APIs write that atom directly; do not also add on[State]Change.
- Use
state.<slice> plus its matching on[State]Change for simple React-controlled state or Table V8-style integrations. Always resolve both raw values and updater functions.
External atoms take precedence over external state; external state synchronizes into the internal base atom. Do not declare the same slice in multiple ownership options and rely on precedence as application logic. The global Table V8 onStateChange callback is gone in Table V9; control slices individually or subscribe to table.store to observe all state.
Initialize, Update, and Reset
Prefer feature methods over direct atom writes because feature methods preserve related behavior. table.baseAtoms.pagination.set(...) is a low-level escape hatch only for internally owned state; write the supplied external atom when atoms.pagination owns the slice.
Feature reset methods reset to table.initialState by default:
table.resetSorting()
table.resetPagination()
table.resetPagination(true)
Slice reset methods flow through that feature's updater and can update an external owner. Core table.reset() resets internal base atoms, so it is not the primary reset mechanism for externally owned atoms.
Use feature-specific types for owned slices and infer the full state from the feature set:
import type { PaginationState, TableState } from '@tanstack/react-table'
type AppTableState = TableState<typeof features>
const initialPagination: PaginationState = { pageIndex: 0, pageSize: 20 }
Common Mistakes
HIGH Treating a snapshot as subscription
Wrong:
const count = Object.keys(table.atoms.rowSelection.get()).length
Correct:
const count = Object.keys(table.state.rowSelection).length
atoms.*.get() and table.store.state return current values but do not subscribe a React render.
Source: packages/react-table/src/useTable.ts
HIGH Supplying only the change callback
Wrong:
const table = useTable({
features,
columns,
data,
onRowSelectionChange: setRowSelection,
})
Correct:
const table = useTable({
features,
columns,
data,
state: { rowSelection },
onRowSelectionChange: setRowSelection,
})
Once a callback takes ownership, the corresponding controlled value must be written back.
Source: docs/framework/react/guide/table-state.md
HIGH Hiding builder reads from the React Compiler
Wrong:
function SelectionCell({ row }) {
return (
<input
type="checkbox"
checked={row.getIsSelected()}
onChange={row.getToggleSelectedHandler()}
/>
)
}
Correct:
import { Subscribe } from '@tanstack/react-table'
function SelectionCell({ row }) {
return (
<Subscribe
source={row.table.atoms.rowSelection}
selector={(selection) => selection[row.id]}
>
{(selected) => (
<input
type="checkbox"
checked={!!selected}
onChange={row.getToggleSelectedHandler()}
/>
)}
</Subscribe>
)
}
With the default selector, useTable returns a fresh React-facing table reference on state changes. The remaining hazard is a nested component receiving only a stable core table, row, cell, column, or header object and hiding a state read behind one of its methods. Keep Subscribe inside that component, or pass its selected value to the child as a changing prop. An outer Subscribe that ignores the selected value is not enough.
Inside cell and header render contexts, table is typed as core Table, so import standalone Subscribe. Use source={table.store} with a selector for multiple slices, or a specific atom for the narrowest boundary.
Source: docs/framework/react/guide/react-compiler.md
MEDIUM Optimizing every cell preemptively
Wrong:
<table.Subscribe source={table.atoms.rowSelection}>
{() => <Cell cell={cell} />}
</table.Subscribe>
Correct:
<Cell cell={cell} />
Default useTable state selection is the simpler starting point; introduce fine-grained boundaries where measurement or compiler behavior justifies them.
Source: docs/framework/react/guide/table-state.md
API Discovery
Inspect node_modules/@tanstack/react-table/dist/useTable.d.ts and Subscribe.d.ts. Core atom precedence and state slices live under node_modules/@tanstack/table-core/dist/.