Perform a complete @tanstack/react-table v8-to-v9 migration: hook and feature architecture, row-model slots, React state and subscriptions, rendering, composable tables, type helpers, and every shared API rename and semantic change. Use for migration plans, implementation, or audits. Treat useLegacyTable only as a deprecated temporary bridge.
Perform a complete @tanstack/react-table v8-to-v9 migration: hook and feature architecture, row-model slots, React state and subscriptions, rendering, composable tables, type helpers, and every shared API rename and semantic change. Use for migration plans, implementation, or audits. Treat useLegacyTable only as a deprecated temporary bridge.
Read @tanstack/table-core#migrate-v8-to-v9, getting-started, and table-state. Use this skill as the exhaustive migration checklist, not as general API documentation. Inspect the installed src files before writing APIs for a different v9 version.
Framework prerequisite: React 18 or newer (react >=18).
Prefer explicit features as the end state. stockFeatures is a useful kitchen-sink migration shortcut, but bundles every stock feature. Do not target useLegacyTable: it is deprecated, React-only, exported from @tanstack/react-table/legacy, and intended only to keep an existing migration moving temporarily.
Complete breaking-change map
Construction and feature registration
v8
v9
useReactTable(options)
useTable({ ...options, features })
Every feature bundled automatically
Register used *Feature objects with tableFeatures()
In the registry slots, register individually imported built-ins (filterFn_includesString, sortFn_alphanumeric, aggregationFn_sum, and so on) under their conventional keys alongside custom functions; the full filterFns/sortFns/aggregationFns registry objects still work but bundle every built-in.
Declare each prerequisite feature before its row-model slot in the same tableFeatures() call. Available stock features are cellSelectionFeature, columnFilteringFeature, globalFilteringFeature, rowSortingFeature, rowPaginationFeature, rowSelectionFeature, rowExpandingFeature, rowPinningFeature, columnPinningFeature, columnVisibilityFeature, columnOrderingFeature, columnSizingFeature, columnResizingFeature, rowAggregationFeature, columnGroupingFeature, and columnFacetingFeature. Aggregation is independent from grouping: register rowAggregationFeature for aggregation APIs and add columnGroupingFeature only for grouped rows.
State and React subscriptions
v8
v9
table.getState()
table.state, table.store.state, or table.atoms.<slice>.get()
Top-level onStateChange
Per-slice onSortingChange, onPaginationChange, etc., or table.store.subscribe() for all changes
Broad component updates
Default useTable selector still subscribes to all registered state; narrow with a selector, table.Subscribe, or useSelector(table.atoms.<slice>)
Framework state only
Optional writable atoms through options.atoms
Controlled state plus per-slice callbacks remains valid:
External atoms override the same slice in state; table setters write directly to them, and table.reset() does not reset them. Do not supply an atom, controlled value, and callback for the same slice without intentionally applying that precedence.
Rendering and composition
flexRender(def, context) still works. Prefer <table.FlexRender cell={cell} />, <table.FlexRender header={header} />, or the standalone <FlexRender ... /> for the v9 component form.
Use tableOptions() to type reusable partial option objects.
Use createTableHook() only when several tables share features, row models, defaults, and registered components. It returns app-specific helpers such as useAppTable, createAppColumnHelper, and table/cell/header context hooks; it is not required for one-off tables.
Invoke row, cell, column, header, and related methods through their instance. Their methods now live on prototypes, so destructuring, object spread, Object.keys, and JSON.stringify do not preserve/expose them. Table-instance methods are not affected.
TypeScript and helper changes
v8
v9
createColumnHelper<Person>()
createColumnHelper<typeof features, Person>()
Plain column array
Prefer columnHelper.columns([...]) to preserve each nested column's TValue
ColumnDef<TData>
ColumnDef<TFeatures, TData, TValue>
Column<TData>
Column<TFeatures, TData, TValue>
Table<TData> / Row<TData>
Table<TFeatures, TData> / Row<TFeatures, TData>
Cell<TData, TValue>
Cell<TFeatures, TData, TValue>
Global TableMeta<TData> / ColumnMeta<TData, TValue>
Add TFeatures first, or register per-table tableMeta / columnMeta with metaHelper()
Register filterFns, sortFns, aggregationFns, and filterMeta slots
RowData = unknown
Row data must be a record or array
Infer TFeatures with typeof features. If deliberately using stockFeatures, use StockFeatures. Do not manually propagate generics when a helper can infer them.
Shared API and behavior changes
Column pinning now uses logical regions, with no deprecated aliases:
This is logical table positioning, not automatic DOM-direction styling. Use CSS logical inset properties for sticky layouts. columnResizeDirection is unchanged.
Other exact changes:
v8
v9
Table option enablePinning
enableColumnPinning plus enableRowPinning; per-column enablePinning remains
Combined ColumnSizing
columnSizingFeature; add columnResizingFeature for interaction
columnSizingInfo
columnResizing
setColumnSizingInfo()
setColumnResizing()
onColumnSizingInfoChange
onColumnResizingChange
sortingFn
sortFn
column.getSortingFn()
column.getSortFn()
column.getAutoSortingFn()
column.getAutoSortFn()
SortingFn / SortingFns / sortingFns
SortFn / SortFns / sortFns
row._getAllCellsByColumnId()
row.getAllCellsByColumnId()
table._getPinnedRows()
getTopRows(), getCenterRows(), or getBottomRows()
table._getFacetedRowModel()
Public faceting APIs on the relevant column/table
table._getFacetedMinMaxValues()
getFacetedMinMaxValues()
table._getFacetedUniqueValues()
getFacetedUniqueValues()
All other underscore-prefixed internals are removed. getIsSomeRowsSelected() and getIsSomePageRowsSelected() now mean at least one, including when all are selected. Compute indeterminate state with getIsSomeRowsSelected() && !getIsAllRowsSelected() or getIsSomePageRowsSelected() && !getIsAllPageRowsSelected().
Migration procedure
Upgrade imports and replace useReactTable with useTable.
Inventory every used state slice, table/column/row method, row model, function registry, and internal _ API.
Build tableFeatures() with the corresponding features first, followed by row-model and registry slots; remove getCoreRowModel.
Apply every mapping above, including physical-to-logical pinning and the sizing/resizing split.
Add typeof features to helpers and explicit public types; migrate meta and function registry augmentation.
Replace getState() and onStateChange; choose internal, controlled per-slice, or external-atom ownership deliberately.
Audit destructured object methods and shallow clones of rows/cells/columns/headers.
Migrate rendering and optionally introduce tableOptions, table.Subscribe, or createTableHook where they solve an actual composition/render boundary.
Type-check, then exercise sorting, filtering, grouping, pagination, expansion, pinning, resizing, selection, and controlled/server-side flows that the table uses.
Remove stockFeatures after the feature audit if bundle specificity matters; remove useLegacyTable rather than treating it as the destination.
Final migration checklist
Replace useReactTable with useTable; remove any temporary useLegacyTable endpoint.
Register every used stock feature explicitly and put each prerequisite before its dependent slot.
Remove getCoreRowModel; move all eight optional row-model factories into tableFeatures.
Move filterFns, sortFns, aggregationFns, and filterMeta into feature slots.
Replace table.getState() and top-level onStateChange; choose selectors, per-slice callbacks, store subscription, or external atoms deliberately.
Audit external-atom precedence/reset ownership and every controlled slice update path.
Replace destructured, spread, serialized, or bare-callback row/cell/column/header methods.
Replace every pinning state key, argument, comparison, method family, and sticky CSS use of left/right with start/end.
Split enablePinning; split sizing/resizing and rename its state, setter, and callback.
Apply every sorting option, method, type, interface, and built-in registry rename.
Remove each listed underscore-prefixed internal API and use the public replacement.
Rebuild indeterminate selection checks with the matching all-selected predicate.
Update helpers and explicit public types for TFeatures; use columns() and StockFeatures where applicable.
Update meta generics or per-table meta slots; replace function/meta augmentation with registry slots.
Ensure RowData is a record or array.
Migrate FlexRender usage; adopt or only where repeated composition warrants it.
Common migration failures
An API is missing because its feature was not registered, not because v9 removed it.
A row model is placed in table options or an obsolete rowModels object instead of its feature slot.
A controlled value is supplied without its matching per-slice callback, freezing that slice.
A React parent still re-renders for every table update because the default selector was retained while assuming atom reads alone narrowed it.
An extracted row.getValue, cell.getContext, or column/header method loses this.
Sticky pinning is renamed in state but not in CSS or every header/row sizing call.
An indeterminate selection checkbox stays indeterminate when all rows are selected.
API discovery
Inspect node_modules/@tanstack/react-table/dist/index.d.ts and node_modules/@tanstack/table-core/dist/index.d.ts for the installed v9 exports and types. Inspect dist/legacy.d.ts only to identify temporary bridge code that remains to be removed.
column.getAfter('end')
column.getIndex('left' | 'right')
column.getIndex('start' | 'end')
tableOptions
createTableHook
Type-check and test every enabled client/manual feature flow, including LTR/RTL pinning and resizing.
Audit away temporary stockFeatures usage when explicit tree-shaking is the intended end state.