Perform a complete TanStack Table v8-to-v9 migration audit: feature registration, row-model and function-registry slots, state/store changes, prototype methods, column pinning and resizing renames, sorting and selection semantics, removed internals, helpers, meta typing, and generic changes. Load this shared inventory before the installed framework adapter's migration skill.
60
71%
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
Fix and improve this skill with Tessl
tessl review fix ./packages/table-core/skills/migrate-v8-to-v9/SKILL.mdApply 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.
stockFeatures usage 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.
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.
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:
columnResizingFeature requires columnSizingFeature.globalFilteringFeature requires columnFilteringFeature.aggregationFns requires rowAggregationFeature; grouped aggregation uses both rowAggregationFeature and columnGroupingFeature.V8 get*RowModel() table options and the earlier-v9-beta 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.
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.
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.
V9 beta.38 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'.
V8's combined sizing feature became two tree-shakeable features:
columnSizingFeature for sizes, offsets, and total-size APIs.columnResizingFeature for drag handles and transient interaction state.columnResizingFeature cannot stand alone.| V8 | V9 |
|---|---|
columnSizingInfo state | columnResizing state |
setColumnSizingInfo(...) | setColumnResizing(...) |
onColumnSizingInfoChange | onColumnResizingChange |
The current source spelling is setColumnResizing with an uppercase C.
| 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.
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.
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.
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().
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.
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.
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.
features object to every table.stockFeatures only as a temporary parity aid and record an explicit-feature follow-up.getCoreRowModel() unless supplying a deliberate custom coreRowModel slot.get*RowModel() options or earlier-beta rowModels entries to create*RowModel() feature slots.filterFns, sortingFns/sortFns, and aggregationFns into feature slots, registering individually imported built-ins; pass no registries to factories.rowAggregationFeature independently and migrate custom aggregation callables to context-based AggregationFnDef definitions.columnFilteringFeature before global filtering and filter/facet dependencies.columnSizingFeature before columnResizingFeature.table.getState() and top-level onStateChange according to the adapter state guide.left/right key, argument, comparison, method family, and sticky CSS declaration with logical start/end equivalents.enablePinning; preserve column-def enablePinning where intended.sortingFn/SortingFn/sortingFns spelling with its v9 sort* spelling.TFeatures to unavoidable explicit core types, helpers, and retained meta augmentation; otherwise restore inference.RowData constraint.any/casts added merely to suppress migration failures.useLegacyTable after the incremental migration step that required it.If table.nextPage, column.toggleSorting, or a state slice disappears, add the associated feature. Do not cast the table to a broader type.
Do not combine v8 get*RowModel options, an earlier beta's rowModels object, or physical pinning names with the current tableFeatures slots.
stockFeatures or useLegacyTable as the finished migrationBoth obscure missing feature decisions; useLegacyTable is deprecated and React-only. Reach behavior parity, then complete the explicit v9 setup.
Core concepts are shared, but reactive reads, constructors, and rendering helpers are not. Load the package-local adapter skills.
Use the installed version, not main-branch memory:
node_modules/@tanstack/table-core/dist/index.d.ts for exports.dist/types/TableFeatures.d.ts for valid slots and prerequisites.dist/features/<feature>/*.types.d.ts for current options, state, and APIs.dist/index.d.ts and its migration skill for entrypoints and rendering.dist/legacy.d.ts only 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 beta.
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.