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.
75
94%
Does it follow best practices?
Run evals on this skill
Adds up to 20 points to the overall score
View guide
Passed
No findings from the security scan
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 beta.
Framework prerequisite: React 18 or newer (react >=18).
import {
columnFilteringFeature,
createFilteredRowModel,
createSortedRowModel,
filterFn_includesString,
rowSortingFeature,
sortFn_alphanumeric,
tableFeatures,
useTable,
} from '@tanstack/react-table'
const features = tableFeatures({
columnFilteringFeature,
rowSortingFeature,
filteredRowModel: createFilteredRowModel(),
sortedRowModel: createSortedRowModel(),
filterFns: { includesString: filterFn_includesString },
sortFns: { alphanumeric: sortFn_alphanumeric },
})
const table = useTable({ features, columns, data })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.
| v8 | v9 |
|---|---|
useReactTable(options) | useTable({ ...options, features }) |
| Every feature bundled automatically | Register used *Feature objects with tableFeatures() |
getCoreRowModel() option | Remove it; the core row model is automatic |
getFilteredRowModel() option | filteredRowModel: createFilteredRowModel() feature slot |
getSortedRowModel() option | sortedRowModel: createSortedRowModel() feature slot |
getPaginationRowModel() option | paginatedRowModel: createPaginatedRowModel() feature slot |
getExpandedRowModel() option | expandedRowModel: createExpandedRowModel() feature slot |
getGroupedRowModel() option | groupedRowModel: createGroupedRowModel() feature slot |
getFacetedRowModel() option | facetedRowModel: createFacetedRowModel() feature slot |
getFacetedMinMaxValues() option | facetedMinMaxValues: createFacetedMinMaxValues() feature slot |
getFacetedUniqueValues() option | facetedUniqueValues: createFacetedUniqueValues() feature slot |
sortingFns table option | sortFns slot in tableFeatures() |
filterFns table option/factory argument | filterFns slot in tableFeatures() |
aggregationFns table option/factory argument | aggregationFns slot in tableFeatures() |
Early-v9-beta rowModels: { ... } | Named row-model slots directly in 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.
| 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:
const [sorting, setSorting] = useState<SortingState>([])
const table = useTable({
features,
columns,
data,
state: { sorting },
onSortingChange: setSorting,
})For fine-grained rendering, pass a selector as the second useTable argument or select closer to the consumer:
const table = useTable(options, () => null)
<table.Subscribe selector={state => state.pagination}>
{pagination => <span>Page {pagination.pageIndex + 1}</span>}
</table.Subscribe>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.
flexRender(def, context) still works. Prefer <table.FlexRender cell={cell} />, <table.FlexRender header={header} />, or the standalone <FlexRender ... /> for the v9 component form.tableOptions() to type reusable partial option objects.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.Object.keys, and JSON.stringify do not preserve/expose them. Table-instance methods are not affected.| 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() |
Augment FilterFns, SortFns, AggregationFns, FilterMeta | 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.
Column pinning now uses logical regions, with no deprecated aliases:
| v8 / pre-beta.38 | v9 beta.38+ |
|---|---|
columnPinning.left / .right | .start / .end |
column.pin('left' | 'right') | column.pin('start' | 'end') |
getIsPinned() === 'left' | 'right' | 'start' | 'end' |
row.getLeftVisibleCells() / getRightVisibleCells() | getStartVisibleCells() / getEndVisibleCells() |
getLeftHeaderGroups() / getRightHeaderGroups() | getStartHeaderGroups() / getEndHeaderGroups() |
getLeftFooterGroups() / getRightFooterGroups() | getStartFooterGroups() / getEndFooterGroups() |
getLeftFlatHeaders() / getRightFlatHeaders() | getStartFlatHeaders() / getEndFlatHeaders() |
getLeftLeafHeaders() / getRightLeafHeaders() | getStartLeafHeaders() / getEndLeafHeaders() |
getLeftLeafColumns() / getRightLeafColumns() | getStartLeafColumns() / getEndLeafColumns() |
getLeftVisibleLeafColumns() / getRightVisibleLeafColumns() | getStartVisibleLeafColumns() / getEndVisibleLeafColumns() |
getLeftTotalSize() / getRightTotalSize() | getStartTotalSize() / getEndTotalSize() |
column.getStart('left') | column.getStart('start') |
column.getAfter('right') | column.getAfter('end') |
column.getIndex('left' | 'right') | column.getIndex('start' | 'end') |
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().
useReactTable with useTable._ API.tableFeatures() with the corresponding features first, followed by row-model and registry slots; remove getCoreRowModel.typeof features to helpers and explicit public types; migrate meta and function registry augmentation.getState() and onStateChange; choose internal, controlled per-slice, or external-atom ownership deliberately.tableOptions, table.Subscribe, or createTableHook where they solve an actual composition/render boundary.stockFeatures after the feature audit if bundle specificity matters; remove useLegacyTable rather than treating it as the destination.useReactTable with useTable; remove any temporary useLegacyTable endpoint.getCoreRowModel; move all eight optional row-model factories into tableFeatures.filterFns, sortFns, aggregationFns, and filterMeta into feature slots.table.getState() and top-level onStateChange; choose selectors, per-slice callbacks, store subscription, or external atoms deliberately.enablePinning; split sizing/resizing and rename its state, setter, and callback.TFeatures; use columns() and StockFeatures where applicable.RowData is a record or array.tableOptions or createTableHook only where repeated composition warrants it.stockFeatures usage when explicit tree-shaking is the intended end state.rowModels object instead of its feature slot.row.getValue, cell.getContext, or column/header method loses this.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.
9e523bc
If you maintain this skill, you can claim it as your own. Once claimed, you can manage eval scenarios, bundle related skills, attach documentation or rules, and ensure cross-agent compatibility.