| name | react/production-readiness |
| description | Ship-ready optimizations for `@tanstack/react-table` v9: tree-shake the bundle by registering ONLY the `_features` you actually use; memoize `_features`, `data`, and `columns` for stable identity; replace `(state) => state` with narrow selectors or per-slice `useSelector(table.atoms.<slice>)` subscriptions; and push state-driven re-renders down the tree with `<table.Subscribe>` / `<Subscribe>` so the expensive table body doesn't re-render every time you toggle a sort indicator. Don't over-optimize small tables — the default selector + inline rendering is fine until measured perf demands more.
|
| type | lifecycle |
| library | tanstack-table |
| framework | react |
| library_version | 9.0.0-alpha.48 |
| requires | ["setup","state-management","react/table-state"] |
| sources | ["TanStack/table:docs/guide/features.md","TanStack/table:docs/framework/react/guide/table-state.md","TanStack/table:examples/react/basic-subscribe/src/main.tsx","TanStack/table:examples/react/basic-external-atoms/src/main.tsx","TanStack/table:examples/react/kitchen-sink/src/main.tsx"] |
This skill builds on tanstack-table/state-management and tanstack-table/react/table-state. Read those first — _features tree-shaking and the atom reactivity model are the foundation; this skill is about which of the patterns introduced there you actually need in production.
When to optimize
The default useTable selector is (state) => state — the component re-renders on any state change. That's correct and ergonomic, and for tables with a few hundred rows and basic features it's the right default. Don't reach for <Subscribe> walls or per-slice atom subscriptions until you've measured a problem (slow keystrokes in a filter input, dropped frames during scrolling, long-running renders). On small tables the optimization noise costs more than it saves.
Setup — stable references
The biggest single perf win is keeping _features, _rowModels, columns, and data references stable across renders. Internal memoization keys off identity, so a new object every render forces full recomputation.
const _features = tableFeatures({ rowSortingFeature, rowPaginationFeature })
const _rowModels = {
sortedRowModel: createSortedRowModel(sortFns),
paginatedRowModel: createPaginatedRowModel(),
}
const columnHelper = createColumnHelper<typeof _features, Person>()
const columns = columnHelper.columns([
columnHelper.accessor('firstName', { header: 'First' }),
columnHelper.accessor('age', { header: 'Age' }),
])
const EMPTY: Person[] = []
function MyTable({ data }: { data: Person[] | undefined }) {
const table = useTable({
_features,
_rowModels,
columns,
data: data ?? EMPTY,
})
}
Core Patterns
1. Tree-shake _features to only what you use
Avoid stockFeatures in production. A sort-only table is ~6–7kb registered explicitly versus ~15–20kb if you import the whole stock set.
const _features = tableFeatures({
rowSortingFeature,
rowPaginationFeature,
})
const _features = tableFeatures(stockFeatures)
Source: docs/guide/features.md; maintainer guidance.
2. Narrow the useTable selector
(state) => state re-renders the holding component on any state change. If only one component cares about one slice, pass a narrow selector — or pass () => null and rely on <table.Subscribe> walls inside.
const table = useTable({ _features, _rowModels, columns, data }, (state) => ({
sorting: state.sorting,
pagination: state.pagination,
}))
const table = useTable(opts, () => null)
Source: examples/react/basic-subscribe/src/main.tsx (uses () => null).
3. Push re-renders down with <table.Subscribe>
A noisy footer that re-renders on every keystroke in a filter doesn't need to re-render the whole <TableBody>. Wrap each consumer in <table.Subscribe> with its own selector.
function MyTable({ data, columns }) {
const table = useTable(
{ _features, _rowModels, columns, data },
() => null,
)
return (
<>
<TableBody table={table} /> {/* heavy — keep stable */}
{/* Footer re-renders only on pagination changes */}
<table.Subscribe selector={(s) => s.pagination}>
{(pagination) => <PageFooter pagination={pagination} table={table} />}
</table.Subscribe>
</>
)
}
Source: examples/react/basic-subscribe/src/main.tsx.
4. Per-slice useSelector(table.atoms.<slice>) for narrowest scope
Even narrower than <table.Subscribe>: subscribe a leaf component to a single atom. Skips constructing a state snapshot entirely.
import { useSelector } from '@tanstack/react-store'
function SelectedCount({ table }) {
const selection = useSelector(table.atoms.rowSelection)
return <span>{Object.keys(selection).length} selected</span>
}
Source: examples/react/basic-external-atoms/src/main.tsx.
5. React Compiler — read state via <Subscribe> in nested components
The compiler can't see through the table closure, so reads via builder APIs (column.getIsPinned(), row.getIsSelected()) in memoized child components go stale. Wrap them in <Subscribe> (see tanstack-table/react/react-subscribe-compiler-compat).
6. Virtualization in the deepest possible component
Keep useVirtualizer in the deepest component (TableBody, not App). Any state change in the holder of the virtualizer re-runs it and tanks scroll perf. See tanstack-table/react/compose-with-tanstack-virtual.
Common Mistakes
HIGH Using stockFeatures in production
Wrong:
import { useTable, stockFeatures, tableFeatures } from '@tanstack/react-table'
const _features = tableFeatures(stockFeatures)
Correct:
import {
useTable,
tableFeatures,
rowSortingFeature,
rowPaginationFeature,
} from '@tanstack/react-table'
const _features = tableFeatures({ rowSortingFeature, rowPaginationFeature })
Tree-shaking via _features is one of the headline reasons for the v9 rewrite. stockFeatures exists for migration / "everything on" smoke tests, not production.
Source: maintainer guidance; docs/guide/features.md.
HIGH Unstable _features / _rowModels / columns references
Wrong:
function MyTable({ data }) {
const _features = tableFeatures({ rowSortingFeature })
const _rowModels = { sortedRowModel: createSortedRowModel(sortFns) }
const table = useTable({ _features, _rowModels, columns, data })
}
Correct:
const _features = tableFeatures({ rowSortingFeature })
const _rowModels = { sortedRowModel: createSortedRowModel(sortFns) }
function MyTable({ data }) {
const table = useTable({ _features, _rowModels, columns, data })
}
Internal memoization keys off identity. A new object every render busts memos and forces full recomputation.
Source: FAQ #1; examples/react/basic-use-table/src/main.tsx.
HIGH data={rows ?? []} in JSX
Wrong:
<MyTable data={query.data?.rows ?? []} columns={columns} />
Correct:
const EMPTY: Person[] = []
<MyTable data={query.data?.rows ?? EMPTY} columns={columns} />
const data = React.useMemo(() => query.data?.rows ?? [], [query.data])
The ?? [] produces a new array identity each render, busting internal memos that depend on data reference.
Source: examples/react/with-tanstack-query/src/main.tsx.
MEDIUM Leaving (state) => state when only one component cares
Wrong:
const table = useTable(opts)
return <DeeplyNestedTable table={table} />
Correct:
const table = useTable(opts, () => null)
return (
<>
<TableBody table={table} />
<table.Subscribe selector={(s) => s.pagination}>
{(p) => <PageFooter pagination={p} />}
</table.Subscribe>
</>
)
Once you've measured a problem, narrow the top selector and add <table.Subscribe> walls around the components that actually need state.
Source: examples/react/basic-subscribe/src/main.tsx.
MEDIUM Subscribing to the whole table.store when a single atom would do
Wrong:
<table.Subscribe selector={(s) => s.rowSelection}>
{(rs) => <span>{Object.keys(rs).length} selected</span>}
</table.Subscribe>
Correct:
import { useSelector } from '@tanstack/react-store'
function SelectedCount({ table }) {
const selection = useSelector(table.atoms.rowSelection)
return <span>{Object.keys(selection).length} selected</span>
}
<table.Subscribe> still selects from table.store.state (the full state). For a single slice, useSelector(table.atoms.X) skips even constructing the snapshot.
Source: docs/framework/react/guide/table-state.md.
MEDIUM Hoisting heavy table state reads above virtualizers
Wrong:
function App() {
const rowVirtualizer = useVirtualizer({
})
const table = useTable(opts)
return <TableBody table={table} virtualizer={rowVirtualizer} />
}
Correct:
function App() {
const tableContainerRef = React.useRef<HTMLDivElement>(null)
const table = useTable(opts)
return (
<div ref={tableContainerRef} style={{ overflow: 'auto', height: 800 }}>
<TableBody table={table} tableContainerRef={tableContainerRef} />
</div>
)
}
function TableBody({ table, tableContainerRef }) {
const rowVirtualizer = useVirtualizer({
})
}
The virtualizer in the deepest component avoids re-running on unrelated state changes.
Source: examples/react/virtualized-rows/src/main.tsx.
MEDIUM Premature <Subscribe> / narrow selectors on small tables
Wrong:
header: ({ table }) => (
<Subscribe source={table.store} selector={(s) => s.sorting}>
{() => <SortHeader />}
</Subscribe>
)
Correct:
const table = useTable({ _features, _rowModels, columns, data })
Advanced state-management patterns are for advanced cases. On small tables the boundary churn costs more than it saves.
Source: maintainer guidance (Phase 4).
See Also
tanstack-table/react/table-state — the API surface this skill optimizes against.
tanstack-table/react/react-subscribe-compiler-compat — required reading if React Compiler is on.
tanstack-table/react/compose-with-tanstack-store — fine-grained subscriptions via external atoms.
tanstack-table/react/compose-with-tanstack-virtual — row/column virtualization patterns.
tanstack-table/react/compose-with-tanstack-devtools — /production import for live devtools in prod.