@tanstack/react-table
Version:
Headless UI for building powerful tables & datagrids for React.
227 lines (186 loc) • 18.7 kB
Markdown
---
name: migrate-v8-to-v9
description: >
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.
metadata:
type: lifecycle
library: '@tanstack/react-table'
library_version: '9.0.0'
framework: react
requires:
- '@tanstack/table-core#migrate-v8-to-v9'
- getting-started
- table-state
sources:
- 'TanStack/table:docs/framework/react/guide/migrating.md'
- 'TanStack/table:packages/react-table/src/index.ts'
- 'TanStack/table:packages/react-table/src/legacy.ts'
- 'TanStack/table:examples/react/basic-use-table'
---
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`).
## Target architecture
```tsx
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.
## 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()` |
| `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()` |
| Removed `rowModels: { ... }` object | 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.
### 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:
```tsx
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:
```tsx
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.
### 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()` |
| 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.
### Shared API and behavior changes
Column pinning now uses logical regions, with no deprecated aliases:
| v8 | v9 |
| -------------------------------------------------------------- | ------------------------------------------------------------- |
| `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()`.
## Migration procedure
1. Upgrade imports and replace `useReactTable` with `useTable`.
2. Inventory every used state slice, table/column/row method, row model, function registry, and internal `_` API.
3. Build `tableFeatures()` with the corresponding features first, followed by row-model and registry slots; remove `getCoreRowModel`.
4. Apply every mapping above, including physical-to-logical pinning and the sizing/resizing split.
5. Add `typeof features` to helpers and explicit public types; migrate meta and function registry augmentation.
6. Replace `getState()` and `onStateChange`; choose internal, controlled per-slice, or external-atom ownership deliberately.
7. Audit destructured object methods and shallow clones of rows/cells/columns/headers.
8. Migrate rendering and optionally introduce `tableOptions`, `table.Subscribe`, or `createTableHook` where they solve an actual composition/render boundary.
9. Type-check, then exercise sorting, filtering, grouping, pagination, expansion, pinning, resizing, selection, and controlled/server-side flows that the table uses.
10. 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 `tableOptions` or `createTableHook` only where repeated composition warrants it.
- [ ] 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.
## 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.