Apply this complete shared inventory before loading the installed adapter's migrate-v8-to-v9 skill. The adapter skill owns hook/controller construction, reactive inputs, rendering helpers, and framework subscription APIs.
Migration strategy
- Make the v8 table pass its existing tests before changing it.
- Migrate construction and features while preserving behavior.
- Let TypeScript expose missing features and stale names.
- Migrate state ownership and rendering through the adapter skill.
- Test every enabled client/server row-model stage and interaction.
- Replace temporary
stockFeaturesusage with explicit features when practical.
Treat useLegacyTable as a deprecated, React-only emergency bridge. It bundles every feature, can exceed the v8 bundle, and must not become the target architecture. Import it only from @tanstack/react-table/legacy when an existing incremental migration requires it.
Minimal v9 shape
import {
createFilteredRowModel,
createSortedRowModel,
columnFilteringFeature,
filterFn_includesString,
rowSortingFeature,
sortFn_alphanumeric,
tableFeatures,
} from '@tanstack/table-core'
export const features = tableFeatures({
columnFilteringFeature,
rowSortingFeature,
filteredRowModel: createFilteredRowModel(),
sortedRowModel: createSortedRowModel(),
filterFns: { includesString: filterFn_includesString },
sortFns: { alphanumeric: sortFn_alphanumeric },
})
Pass features to the adapter's v9 table constructor. Define it statically outside render/setup work when possible.
Complete shared breaking-change inventory
1. Register every non-core feature explicitly
V8 bundled all stock features. V9 exposes an API only when its feature is present in tableFeatures({...}).
| Capability | V9 feature |
|---|---|
| Aggregation | rowAggregationFeature |
| Cell selection | cellSelectionFeature |
| Column faceting | columnFacetingFeature |
| Column filtering | columnFilteringFeature |
| Column ordering | columnOrderingFeature |
| Column pinning | columnPinningFeature |
| Column sizes and offsets | columnSizingFeature |
| Column visibility | columnVisibilityFeature |
| Global filtering | globalFilteringFeature |
| Grouping | columnGroupingFeature |
| Interactive column resizing | columnResizingFeature |
| Pagination | rowPaginationFeature |
| Row expansion | rowExpandingFeature |
| Row pinning | rowPinningFeature |
| Row selection | rowSelectionFeature |
| Sorting | rowSortingFeature |
The core row model and core table/row/column/header/cell behavior are automatic. stockFeatures restores a v8-like all-features surface, but use it as an audit shortcut rather than the default production recommendation.
Honor feature prerequisites in the same tableFeatures call:
columnResizingFeaturerequirescolumnSizingFeature.globalFilteringFeaturerequirescolumnFilteringFeature.- Every row-model or function-registry slot requires its associated feature.
aggregationFnsrequiresrowAggregationFeature; grouped aggregation uses bothrowAggregationFeatureandcolumnGroupingFeature.- Put prerequisite feature properties before dependent slots so inference and diagnostics remain clear.
2. Move row models into feature slots and rename factories
V8 get*RowModel() table options and the removed rowModels object are gone. V9 create*RowModel() factories take no registry arguments and are registered as named feature slots.
| V8 table option | V9 tableFeatures slot |
V9 factory |
|---|---|---|
getCoreRowModel: getCoreRowModel() |
automatic; omit for the built-in model | built-in createCoreRowModel() is the default |
getFilteredRowModel: getFilteredRowModel() |
filteredRowModel |
createFilteredRowModel() |
getSortedRowModel: getSortedRowModel() |
sortedRowModel |
createSortedRowModel() |
getPaginationRowModel: getPaginationRowModel() |
paginatedRowModel |
createPaginatedRowModel() |
getExpandedRowModel: getExpandedRowModel() |
expandedRowModel |
createExpandedRowModel() |
getGroupedRowModel: getGroupedRowModel() |
groupedRowModel |
createGroupedRowModel() |
getFacetedRowModel: getFacetedRowModel() |
facetedRowModel |
createFacetedRowModel() |
getFacetedMinMaxValues: getFacetedMinMaxValues() |
facetedMinMaxValues |
createFacetedMinMaxValues() |
getFacetedUniqueValues: getFacetedUniqueValues() |
facetedUniqueValues |
createFacetedUniqueValues() |
For a custom core model, use the coreRowModel slot rather than restoring the v8 table option.
Move registries from table options or factory arguments into these feature slots:
| V8 | V9 |
|---|---|
sortingFns |
sortFns |
filterFns |
filterFns |
aggregationFns |
aggregationFns |
Register only the built-ins the table references by string name, importing each individually (filterFn_includesString, sortFn_alphanumeric, aggregationFn_sum, and so on) alongside any custom functions. The full registry objects (filterFns, sortFns, aggregationFns exports) still work but bundle every built-in. A slot's keys become the valid string names in column definitions, and 'auto' resolves only registered functions.
Aggregation is independent from grouping. Add rowAggregationFeature for
aggregationFn, aggregatedCell, column.getAggregationValue(options?), and
cell.getIsAggregated. A root total does not require grouping. Convert legacy
custom callables (columnId, leafRows, childRows) => result to
constructAggregationFn({ aggregate: (context) => result, merge? })
definitions. Replace column.getAggregationFn() with
column.getAggregationFns(); arrays in aggregationFn return keyed objects.
Replace the old AggregationFn and CreatedAggregationFn types with
AggregationFnDef. Aggregation row selection is shared across every definition
on a column: maxAggregationDepth defaults to 0, while 1 selects direct
sub-rows and Infinity selects terminal rows. Explicit totals can override it
with the single object signature
column.getAggregationValue({ rows, maxDepth }); positional row and depth
arguments are not supported. All built-ins consume the same selected rows.
Custom definitions can inspect grouped subRows, and merge receives matching
subRowResults. Use table.getMaxSubRowDepth() when a depth should derive from
the deepest structural row in the core model.
3. Migrate state reads and whole-state observation
table.getState() and the top-level onStateChange option are removed. Individual on[Slice]Change callbacks remain.
| V8 need | V9 shared surface |
|---|---|
| Full current snapshot | table.store.state |
| One current slice | table.atoms.<slice>.get() |
| Adapter-selected reactive state | table.state where the adapter exposes it |
| Observe all changes | table.store.subscribe(...) |
| Control one slice | state.<slice> plus on<Slice>Change |
| Externally own one slice | atoms.<slice> with a writable TanStack Store atom |
| Internal state | omit both state.<slice> and atoms.<slice> |
Load the adapter table-state skill before choosing reactive reads; adapters intentionally differ. When both an external atom and state provide a slice, the atom wins. Table writes go directly to that atom, and table.reset() does not reset externally owned atoms.
4. Keep instance methods bound
Row, cell, column, header, and related object methods moved to shared prototypes. Destructuring, passing a bare callback, spreading, Object.keys, and JSON.stringify no longer preserve or reveal those methods.
// v8 code that breaks
const { getValue } = row
rows.map(row.getVisibleCells)
// v9
const value = row.getValue('name')
rows.map((row) => row.getVisibleCells())
Audit all methods extracted from rows, cells, columns, headers, and header groups. Table-instance methods are not subject to this specific migration rule.
5. Replace physical column pinning with logical pinning
V9 has no left/right aliases. Replace all state keys, return-value comparisons, arguments, and API families:
| V8 | V9 |
|---|---|
columnPinning.left / .right |
.start / .end |
column.pin('left' | 'right') |
column.pin('start' | 'end') |
column.getIsPinned() === 'left' | 'right' |
compare with 'start' | 'end' |
row.getLeftVisibleCells() / getRightVisibleCells() |
getStartVisibleCells() / getEndVisibleCells() |
table.getLeftHeaderGroups() / getRightHeaderGroups() |
getStartHeaderGroups() / getEndHeaderGroups() |
table.getLeftFooterGroups() / getRightFooterGroups() |
getStartFooterGroups() / getEndFooterGroups() |
table.getLeftFlatHeaders() / getRightFlatHeaders() |
getStartFlatHeaders() / getEndFlatHeaders() |
table.getLeftLeafHeaders() / getRightLeafHeaders() |
getStartLeafHeaders() / getEndLeafHeaders() |
table.getLeftLeafColumns() / getRightLeafColumns() |
getStartLeafColumns() / getEndLeafColumns() |
table.getLeftVisibleLeafColumns() / getRightVisibleLeafColumns() |
getStartVisibleLeafColumns() / getEndVisibleLeafColumns() |
table.getLeftTotalSize() / getRightTotalSize() |
getStartTotalSize() / getEndTotalSize() |
'left' | 'right' passed to getStart, getAfter, getIndex, or pinned-region helpers |
'start' | 'end' |
This names logical regions; it does not automatically apply DOM direction or sticky CSS. Use logical CSS such as inset-inline-start/insetInlineStart and inset-inline-end/insetInlineEnd. columnResizeDirection remains 'ltr' | 'rtl'.
6. Split column sizing from resizing
V8's combined sizing feature became two tree-shakeable features:
- Register
columnSizingFeaturefor sizes, offsets, and total-size APIs. - Also register
columnResizingFeaturefor drag handles and transient interaction state. columnResizingFeaturecannot stand alone.
| V8 | V9 |
|---|---|
columnSizingInfo state |
columnResizing state |
setColumnSizingInfo(...) |
setColumnResizing(...) |
onColumnSizingInfoChange |
onColumnResizingChange |
The current source spelling is setColumnResizing with an uppercase C.
7. Rename sorting APIs
| V8 | V9 |
|---|---|
column-def sortingFn |
sortFn |
column.getSortingFn() |
column.getSortFn() |
column.getAutoSortingFn() |
column.getAutoSortFn() |
SortingFn |
SortFn |
SortingFns |
SortFns |
built-in sortingFns |
sortFns |
Also move the registry to tableFeatures, as described above.
8. Split the table-level pinning switch
Replace the v8 table option enablePinning with enableColumnPinning and/or enableRowPinning. Do not mechanically rename a column definition's enablePinning: that column-level option still exists.
9. Remove internal APIs and use public surfaces
All underscore-prefixed internals are unsupported and removed. Known migration points include:
| Removed v8 internal | V9 public direction |
|---|---|
row._getAllCellsByColumnId() |
row.getAllCellsByColumnId() |
table._getPinnedRows() |
table.getTopRows(), getCenterRows(), or getBottomRows() |
table._getFacetedRowModel() |
public faceting APIs on the relevant column/table |
table._getFacetedMinMaxValues() |
getFacetedMinMaxValues() |
table._getFacetedUniqueValues() |
getFacetedUniqueValues() |
For any other _ API, do not guess. Inspect the installed v9 package source for the public replacement or redesign the integration.
10. Update row-selection predicates
getIsSomeRowsSelected() and getIsSomePageRowsSelected() now mean at least one, including the all-selected case. They no longer mean “some but not all.” Build an indeterminate checkbox with both predicates:
const indeterminate =
table.getIsSomeRowsSelected() && !table.getIsAllRowsSelected()
For a page checkbox, use table.getIsSomePageRowsSelected() && !table.getIsAllPageRowsSelected().
11. Update TypeScript feature generics and helpers
Most core types add TFeatures before their data/value parameters:
| V8 | V9 |
|---|---|
Column<TData> |
Column<TFeatures, TData, TValue> |
ColumnDef<TData> |
ColumnDef<TFeatures, TData, TValue> |
Table<TData> |
Table<TFeatures, TData> |
Row<TData> |
Row<TFeatures, TData> |
Cell<TData, TValue> |
Cell<TFeatures, TData, TValue> |
createColumnHelper<TData>() |
createColumnHelper<TFeatures, TData>() |
Prefer inference. Use typeof features only where an explicit type boundary is necessary; use StockFeatures when that is genuinely the selected feature set. Wrap column arrays with columnHelper.columns([...]) to preserve individual and nested TValue inference.
RowData is now Record<string, any> | Array<any>, not unknown. Wrap primitive records in an object or array shape.
Global TableMeta/ColumnMeta declaration merging can remain, but add TFeatures as the first generic. Prefer per-table type-only slots where isolation helps:
const features = tableFeatures({
columnFilteringFeature,
tableMeta: metaHelper<MyTableMeta>(),
columnMeta: metaHelper<MyColumnMeta>(),
filterMeta: metaHelper<MyFilterMeta>(),
})
Replace global FilterFns, SortFns, and AggregationFns augmentation with the matching registry slots; their object keys supply the string-literal names. Replace FilterMeta augmentation with the filterMeta slot unless global behavior is intentional.
12. Migrate adapter construction and rendering separately
The following are breaking but not shared core mappings: adapter hook/factory/controller names, Svelte 5 requirements, reactive getter rules, state-selection components/helpers, and FlexRender syntax. Load the installed adapter's migration skill and follow its exact source. Do not apply React names to Preact, Solid, Svelte, Vue, Angular, or Lit.
Optional v9 adoption after parity
Do not confuse new capabilities with required breakages. After the table works, consider tableOptions() for reusable partial configuration, createTableHook() for app-level table conventions, per-slice atoms/subscriptions for narrower rendering, per-table meta slots, and explicit features for smaller bundles.
Complete audit checklist
- Load the installed framework adapter's migration and table-state skills.
- Replace the v8 adapter constructor/hook/controller with its v9 entrypoint.
- Add a stable
featuresobject to every table. - Inventory every feature API used by table, row, column, cell, and header code; register all 16 required stock features.
- Use
stockFeaturesonly as a temporary parity aid and record an explicit-feature follow-up. - Remove
getCoreRowModel()unless supplying a deliberate customcoreRowModelslot. - Move all remaining
get*RowModel()options (or a removedrowModels: {...}object, if present) tocreate*RowModel()feature slots. - Register each dependent feature before its row-model slot.
- Move
filterFns,sortingFns/sortFns, andaggregationFnsinto feature slots, registering individually imported built-ins; pass no registries to factories. - Register
rowAggregationFeatureindependently and migrate custom aggregation callables to context-basedAggregationFnDefdefinitions. - Register
columnFilteringFeaturebefore global filtering and filter/facet dependencies. - Register
columnSizingFeaturebeforecolumnResizingFeature. - Replace
table.getState()and top-levelonStateChangeaccording to the adapter state guide. - Verify every controlled slice has an update path; verify externally owned atoms are reset by their owner.
- Audit destructured, spread, serialized, or bare-callback instance methods.
- Replace every column-pinning
left/rightkey, argument, comparison, method family, and sticky CSS declaration with logical start/end equivalents. - Split table-level
enablePinning; preserve column-defenablePinningwhere intended. - Split sizing/resizing features and rename the resizing state, setter, and callback.
- Replace every
sortingFn/SortingFn/sortingFnsspelling with its v9sort*spelling. - Remove every consumed underscore-prefixed internal API.
- Recheck indeterminate selection logic against the new “at least one” semantics.
- Add
TFeaturesto unavoidable explicit core types, helpers, and retained meta augmentation; otherwise restore inference. - Replace function-registry and filter-meta declaration merging with per-table slots where appropriate.
- Ensure every row is a record or array under the stricter
RowDataconstraint. - Migrate adapter-specific rendering, reactive data inputs, and subscription primitives.
- Type-check without
any/casts added merely to suppress migration failures. - Test sorting, filtering, faceting, grouping, expansion, pagination, selection, ordering, pinning, sizing, and resizing—only where enabled.
- Test client/server ownership for every row-model pipeline stage and ensure manual modes receive already-processed data.
- Test LTR and RTL layouts when column pinning or resizing is enabled.
- Remove
useLegacyTableafter the incremental migration step that required it.
Common migration failures
[CRITICAL] Silencing a missing API instead of registering its feature
If table.nextPage, column.toggleSorting, or a state slice disappears, add the associated feature. Do not cast the table to a broader type.
[CRITICAL] Mixing v8 and v9 configuration styles
Do not combine v8 get*RowModel options, a removed rowModels object, or physical pinning names with the current tableFeatures slots.
[HIGH] Treating stockFeatures or useLegacyTable as the finished migration
Both obscure missing feature decisions; useLegacyTable is deprecated and React-only. Reach behavior parity, then complete the explicit v9 setup.
[HIGH] Copying one adapter's state or rendering API into another
Core concepts are shared, but reactive reads, constructors, and rendering helpers are not. Load the package-local adapter skills.
Installed API discovery
Use the installed version, not main-branch memory:
- Inspect
node_modules/@tanstack/table-core/dist/index.d.tsfor exports. - Inspect
dist/types/TableFeatures.d.tsfor valid slots and prerequisites. - Inspect
dist/features/<feature>/*.types.d.tsfor current options, state, and APIs. - Inspect the installed adapter's
dist/index.d.tsand its migration skill for entrypoints and rendering. - Inspect
dist/legacy.d.tsonly to remove an existing bridge, never to design new v9 code.
If package-manager layout prevents that exact path, resolve the installed package root first. Do not substitute APIs from a different v9 version.