Search docs

Jump to any table example

Discord—

Migrating from TanStack Table v8 to v9

What changes in your shadcn/ui tables when you move to @tanstack/react-table v9, with before and after code taken from the examples on this site. Every example here has a v8 and a v9 version, so you can compare any page side by side.

The fast path: install the v9 version

If you have not installed a component yet, skip the migration. Every registry item has a v9 twin that adds the right dependency and a ready-made features file:

terminal
npx shadcn add https://www.shad-table.dev/r/data-table-v9.json

On a page, switch to the v9 version and the install command updates itself.

1.Register features and row models

v8 bundled every feature and took row models as get*RowModel() options. v9 asks you to say what the table uses: you build a features object once, pass it as `features`, and row models become named slots inside it. getCoreRowModel() is gone; the core row model is automatic.

v8
import {
getCoreRowModel,
getFilteredRowModel,
getSortedRowModel,
useReactTable,
} from '@tanstack/react-table'
const table = useReactTable({
data,
columns,
getCoreRowModel: getCoreRowModel(),
getFilteredRowModel: getFilteredRowModel(),
getSortedRowModel: getSortedRowModel(),
})
v9
import {
columnFilteringFeature,
createFilteredRowModel,
createSortedRowModel,
rowSortingFeature,
tableFeatures,
useTable,
} from '@tanstack/react-table'
const features = tableFeatures({
columnFilteringFeature,
rowSortingFeature,
filteredRowModel: createFilteredRowModel(),
sortedRowModel: createSortedRowModel(),
})
const table = useTable({ features, data, columns })

If a table method is missing in v9, the feature that owns it is usually not registered, not removed. Declare a feature before the row model that depends on it.

2.Add the features type to your column and table types

Every table type now starts with the type of your features object, so TypeScript knows which methods exist. Use `typeof features` rather than writing it out. Row data must also be an object or array, which is why generic components in these examples became `TData extends RowData`.

v8
export const columns: ColumnDef<User>[] = [...]
function PinControls<TData>({
column,
}: {
column: Column<TData, unknown>
}) {}
v9
export const columns: ColumnDef<typeof features, User>[] = [...]
function PinControls<TData extends RowData>({
column,
}: {
column: Column<typeof features, TData, unknown>
}) {}

3.Rename sorting functions

Everything called sortingFn is now sortFn, including the column option, the registry in tableFeatures(), and the type names.

v8
{ accessorKey: 'name', sortingFn: 'alphanumeric' }
v9
{ accessorKey: 'name', sortFn: 'alphanumeric' }

filterFns, sortFns and aggregationFns are registered in tableFeatures() instead of being table options or global type augmentations.

4.Read state from the store

table.getState() is replaced by table.state or table.store.state. A single onStateChange callback no longer exists; use the per-slice callbacks (onSortingChange, onPaginationChange, …) you already use for controlled state, or table.store.subscribe() to hear about every change.

v8
const { pageIndex, pageSize } = table.getState().pagination
v9
const { pageIndex, pageSize } = table.store.state.pagination

By default useTable still re-renders on every state change. To narrow that, pass a selector as the second argument or render <table.Subscribe selector={…}> around only the part that needs the state.

5.Pin columns to start and end

Column pinning uses logical regions instead of physical sides. The state keys, the arguments to column.pin(), the values returned by getIsPinned(), and every left/right method family change together, so search for all of them.

v8
const [pinning, setPinning] = useState({ left: [], right: [] })
column.pin('left')
column.getStart('left')
row.getLeftVisibleCells()
v9
const [pinning, setPinning] = useState({ start: [], end: [] })
column.pin('start')
column.getStart('start')
row.getStartVisibleCells()

This is logical positioning, not automatic RTL styling. Your sticky CSS still decides which physical edge a pinned column sticks to.

6.Check selection and sizing behavior changes

getIsSomeRowsSelected() and getIsSomePageRowsSelected() now mean at least one row, including when all rows are selected. Check the "all" case first, or exclude it, to compute an indeterminate checkbox. Column sizing is split in two: columnSizingFeature for widths and columnResizingFeature for dragging, and the columnSizingInfo state is now columnResizing.

v8
const indeterminate = table.getIsSomeRowsSelected()
v9
const indeterminate =
table.getIsSomeRowsSelected() && !table.getIsAllRowsSelected()

Quick reference

v8v9
useReactTable(options)useTable({ ...options, features })
getCoreRowModel()removed (automatic)
getSortedRowModel()sortedRowModel: createSortedRowModel()
getFilteredRowModel()filteredRowModel: createFilteredRowModel()
getPaginationRowModel()paginatedRowModel: createPaginatedRowModel()
getExpandedRowModel()expandedRowModel: createExpandedRowModel()
table.getState()table.state / table.store.state
onStateChangeper-slice onXChange or table.store.subscribe()
sortingFn / sortingFnssortFn / sortFns
columnPinning { left, right }columnPinning { start, end }
enablePinning (table option)enableColumnPinning + enableRowPinning
columnSizingInfocolumnResizing
ColumnDef<TData>ColumnDef<TFeatures, TData>
createColumnHelper<TData>()createColumnHelper<TFeatures, TData>()

How the examples here handle features

Every v9 example imports one shared features file that registers all stock features, every row model and every built-in function. That keeps behavior identical to v8's "everything included" default, which makes the examples easy to compare. It is not tree-shaken: in your own app, replace it with only the features and row models your table actually uses, as in step 1.

src/lib/table-v9-features.ts
// Shared TanStack Table v9 feature registration used by every /v9 demo page.
// Registers stockFeatures plus every row-model factory and the full built-in
// fn registries, so v9 pages behave like v8's "everything is bundled" default.
// A real app should trim this to only the features/row models it actually
// uses (see the migrate-v8-to-v9 skill shipped in the package) for smaller
// bundles — this file favors parity over tree-shaking.
import {
aggregationFns,
createExpandedRowModel,
createFacetedMinMaxValues,
createFacetedRowModel,
createFacetedUniqueValues,
createFilteredRowModel,
createGroupedRowModel,
createPaginatedRowModel,
createSortedRowModel,
filterFns,
sortFns,
stockFeatures,
tableFeatures,
} from '@tanstack/react-table-v9'
export const v9Features = tableFeatures({
...stockFeatures,
filteredRowModel: createFilteredRowModel(),
sortedRowModel: createSortedRowModel(),
paginatedRowModel: createPaginatedRowModel(),
expandedRowModel: createExpandedRowModel(),
groupedRowModel: createGroupedRowModel(),
facetedRowModel: createFacetedRowModel(),
facetedUniqueValues: createFacetedUniqueValues(),
facetedMinMaxValues: createFacetedMinMaxValues(),
filterFns,
sortFns,
aggregationFns,
})
export type V9Features = typeof v9Features

Migration checklist

  • Replace useReactTable with useTable and pass a features object.
  • Remove getCoreRowModel and move every other row model into a feature slot.
  • Register the features you use; a missing method usually means a missing feature.
  • Add the features type to ColumnDef, Column, Table, Row and Cell types.
  • Rename sortingFn to sortFn, and move filterFns, sortFns and aggregationFns into tableFeatures().
  • Replace getState() and onStateChange.
  • Rename pinning left/right to start/end in state, methods and arguments.
  • Re-check indeterminate selection checkboxes.
  • Call row, cell, column and header methods on the instance: they now live on prototypes, so destructured or spread copies lose them.
  • Type-check, then try sorting, filtering, pagination, selection and any server-side flows by hand.

The @tanstack/react-table v9 package also ships a full migrate-v8-to-v9 guide in its skills folder, with the complete list of renames, for anything not covered here.