Capítulo 22 de 54
"TanStack Table is a state-management coordinator for table state" — v9 backs that state with TanStack Store atoms, so reading current state (table.atoms.x.get()) and reading reactive state that re-renders your component (useTable's selector, or table.Subscribe) are two genuinely different operations, and picking the wrong one is the source of most state-related bugs and perf problems.
initialState, atoms, state, or on[State]Change → the table manages everything internally. Only reach for the mechanisms below when you have a concrete reason (server-driven state, cross-component sharing, fine-grained re-render control).table: table.baseAtoms (internal writable atoms), table.atoms (readonly derived atoms, one per registered slice), table.store (readonly flat snapshot derived from all table.atoms), table.state (React-only — whatever the selector passed as useTable's 2nd argument returns).pagination) only exists on table.atoms/table.store/table.state/initialState/state/atoms if the matching feature (rowPaginationFeature) is registered — referencing an unregistered slice is a type error, not just a runtime undefined.table.atoms.pagination.get() or table.store.state.pagination. Fine during render for a one-off read, but future changes to that value will not re-render the component on their own.useTable is a selector over the store. The default selector selects all registered state (component re-renders on any state change). Pass your own selector — e.g. (state) => ({ pagination: state.pagination }) — to narrow what triggers a re-render, or () => null to opt the component out of table-state re-renders entirely (then subscribe lower in the tree with table.Subscribe).table.Subscribe places a re-render boundary exactly where it's needed — pass selector (over table.store) alone, or add a source prop to subscribe directly to one atom (e.g. table.atoms.rowSelection) and select one value off it, which is the standard pattern for "only this row's checkbox re-renders when selection changes."table.nextPage(), table.setPageSize(n), table.setSorting(...), column.toggleVisibility(), row.toggleSelected()), never by hand-editing the state object — the APIs preserve each feature's own invariants. Direct writes to table.baseAtoms.<slice>.set(...) are a rare low-level escape hatch, and if a slice is externally owned (via atoms), you must write to that external atom instead — table.atoms.<slice> mirrors the external atom, not the internal base atom.initialState sets starting values only (and is what resetX() APIs reset back to) — mutating the initialState object later does not reset live state. Reset APIs take an optional true to reset to the feature's blank/default state instead of initialState. Prefer feature resetX() calls over the low-level table.reset(), which only resets internal base atoms and won't correctly reset externally-owned atoms.useCreateAtom(initialValue) from TanStack Store, passed via the table's atoms option; subscribe elsewhere with useSelector. Table APIs write directly to the atom — no matching on[State]Change needed. Ideal for state that also belongs in a query key (server-driven pagination/sorting/filters).state + on[State]Change (classic v8-style, still supported): plain React state + a setter callback per slice, e.g. state: { sorting } paired with onSortingChange: setSorting. Less fine-grained — a state update re-renders the owning component, and that can't be pushed lower like an atom subscription can.onStateChange is gone. Use per-slice on[State]Change callbacks, external atoms, or subscribe to table.store directly if you truly need "every state change."state; external state syncs into the internal base atom. Don't provide the same slice through more than one channel unless you intend that precedence.SortingState, PaginationState, RowSelectionState, ...) for local state/atoms over hand-rolling shapes; TableState<typeof features> gives the full inferred state shape for a given feature set.// narrow re-renders to one slice
const table = useTable({ features, columns, data }, (state) => ({ pagination: state.pagination }))
// subscribe to one atom, one derived value — classic per-row checkbox pattern
<table.Subscribe source={table.atoms.rowSelection} selector={(sel) => sel[row.id]}>
{(isSelected) => <input type="checkbox" checked={!!isSelected} onChange={row.getToggleSelectedHandler()} />}
</table.Subscribe>
// external atom, shared with a TanStack Query key
const paginationAtom = useCreateAtom<PaginationState>({ pageIndex: 0, pageSize: 10 })
const pagination = useSelector(paginationAtom)
const table = useTable({
features, columns, data: dataQuery.data?.rows ?? [],
atoms: { pagination: paginationAtom }, // table APIs write here directly, no onPaginationChange needed
manualPagination: true,
})
useTable, table.Subscribe with a selector, table.Subscribe scoped to one atom — plus an external atom shared with a data-fetching layer.initialState/atoms/state — until you have a concrete reason to intervene..get()/table.store.state.x = current value, no reactivity. A selector passed to useTable (or table.Subscribe) = reactive, re-render-triggering.setSorting, toggleSelected, nextPage, ...), never by hand-constructing the state object.atoms option) over the classic state/on[State]Change pair — it's the more fine-grained, v9-idiomatic path.initialState/atoms/state for the same slice invites silent precedence bugs.table.Subscribe is required regardless.atoms + query-key pattern in its full server-driven context.table.atoms/table.store/table.baseAtoms introduced at a lower level.