TanStack Table v9 is a major release that introduces significant architectural improvements while maintaining the core table logic you're familiar with. Here are the key changes:
While v9 is a significant upgrade, you don't have to adopt everything at once:
The main change is how you define a table with the useTable hook — specifically the new _features and _rowModels options.
Need to migrate incrementally? Use useLegacyTable — it accepts the v8-style API while using v9 under the hood. This is deprecated and intended only as a temporary migration aid. It includes all features by default, resulting in a larger bundle size.
Legacy APIs live in a separate export. Import core utilities from @tanstack/react-table and legacy-specific APIs from @tanstack/react-table/legacy:
import { flexRender } from '@tanstack/react-table'
import {
useLegacyTable,
getCoreRowModel,
getFilteredRowModel,
getSortedRowModel,
getPaginationRowModel,
legacyCreateColumnHelper,
} from '@tanstack/react-table/legacy'
See the useLegacyTable Guide for full documentation, examples, and type helpers.
The rest of this guide focuses on migrating to the full v9 API and taking advantage of its features.
The hook name has been simplified to be consistent across all TanStack libraries:
// v8
import { useReactTable } from '@tanstack/react-table'
const table = useReactTable(options)
// v9
import { useTable } from '@tanstack/react-table'
const table = useTable(options)
In v9, you must explicitly declare which features and row models your table uses:
// v8
import { useReactTable, getCoreRowModel } from '@tanstack/react-table'
const table = useReactTable({
columns,
data,
getCoreRowModel: getCoreRowModel(),
})
// v9
import { useTable, tableFeatures } from '@tanstack/react-table'
const _features = tableFeatures({}) // Empty = core features only
const table = useTable({
_features,
_rowModels: {}, // Core row model is automatic
columns,
data,
})
Features control what table functionality is available. In v8, all features were bundled. In v9, you import only what you need.
import {
tableFeatures,
// Import only the features you need
columnFilteringFeature,
rowSortingFeature,
rowPaginationFeature,
columnVisibilityFeature,
rowSelectionFeature,
} from '@tanstack/react-table'
// Create a features object (define this outside your component for stable reference)
const _features = tableFeatures({
columnFilteringFeature,
rowSortingFeature,
rowPaginationFeature,
columnVisibilityFeature,
rowSelectionFeature,
})
If you want all features without thinking about it (like v8), import stockFeatures:
import { useTable, stockFeatures } from '@tanstack/react-table'
const table = useTable({
_features: stockFeatures, // All features included
_rowModels: { /* ... */ },
columns,
data,
})
| Feature | Import Name |
|---|---|
| Column Filtering | columnFilteringFeature |
| Global Filtering | globalFilteringFeature |
| Row Sorting | rowSortingFeature |
| Row Pagination | rowPaginationFeature |
| Row Selection | rowSelectionFeature |
| Row Expanding | rowExpandingFeature |
| Row Pinning | rowPinningFeature |
| Column Pinning | columnPinningFeature |
| Column Visibility | columnVisibilityFeature |
| Column Ordering | columnOrderingFeature |
| Column Sizing | columnSizingFeature |
| Column Resizing | columnResizingFeature |
| Column Grouping | columnGroupingFeature |
| Column Faceting | columnFacetingFeature |
Row models are the functions that process your data (filtering, sorting, pagination, etc.). In v9, they're configured via _rowModels instead of get*RowModel options.
| v8 Option | v9 _rowModels Key | v9 Factory Function |
|---|---|---|
| getCoreRowModel() | (automatic) | Not needed — always included |
| getFilteredRowModel() | filteredRowModel | createFilteredRowModel(filterFns) |
| getSortedRowModel() | sortedRowModel | createSortedRowModel(sortFns) |
| getPaginationRowModel() | paginatedRowModel | createPaginatedRowModel() |
| getExpandedRowModel() | expandedRowModel | createExpandedRowModel() |
| getGroupedRowModel() | groupedRowModel | createGroupedRowModel(aggregationFns) |
| getFacetedRowModel() | facetedRowModel | createFacetedRowModel() |
| getFacetedMinMaxValues() | facetedMinMaxValues | createFacetedMinMaxValues() |
| getFacetedUniqueValues() | facetedUniqueValues | createFacetedUniqueValues() |
Several row model factories now accept their processing functions as parameters. This enables better tree-shaking and explicit configuration:
import {
createFilteredRowModel,
createSortedRowModel,
createGroupedRowModel,
filterFns, // Built-in filter functions
sortFns, // Built-in sort functions
aggregationFns, // Built-in aggregation functions
} from '@tanstack/react-table'
const table = useTable({
_features,
_rowModels: {
filteredRowModel: createFilteredRowModel(filterFns),
sortedRowModel: createSortedRowModel(sortFns),
groupedRowModel: createGroupedRowModel(aggregationFns),
paginatedRowModel: createPaginatedRowModel(),
},
columns,
data,
})
// v8
import {
useReactTable,
getCoreRowModel,
getFilteredRowModel,
getSortedRowModel,
getPaginationRowModel,
filterFns,
sortingFns,
} from '@tanstack/react-table'
const table = useReactTable({
columns,
data,
getCoreRowModel: getCoreRowModel(), // used to be called "get*RowModel()"
getFilteredRowModel: getFilteredRowModel(),
getSortedRowModel: getSortedRowModel(),
getPaginationRowModel: getPaginationRowModel(),
filterFns, // used to be passed in as a root option
sortingFns,
})
// v9
import {
useTable,
tableFeatures,
columnFilteringFeature,
rowSortingFeature,
rowPaginationFeature,
createFilteredRowModel,
createSortedRowModel,
createPaginatedRowModel,
filterFns,
sortFns,
} from '@tanstack/react-table'
const _features = tableFeatures({
columnFilteringFeature,
rowSortingFeature,
rowPaginationFeature,
})
const table = useTable({
_features,
_rowModels: {
filteredRowModel: createFilteredRowModel(filterFns), // now called "create*RowModel()" with a Fns parameter
sortedRowModel: createSortedRowModel(sortFns),
paginatedRowModel: createPaginatedRowModel(),
},
columns,
data,
})
In v8, you accessed state via table.getState(). In v9, state is accessed differently:
// v8
const state = table.getState()
const { sorting, pagination } = table.getState()
// v9 - via the store (full state)
const fullState = table.store.state
const { sorting, pagination } = table.store.state
// v9 - via table.state (selected state from your selector)
const table = useTable(options, (state) => ({
sorting: state.sorting,
pagination: state.pagination,
}))
// Now table.state only contains sorting and pagination
const { sorting, pagination } = table.state
The biggest state management improvement is table.Subscribe, which enables fine-grained reactivity:
function MyTable() {
const table = useTable({
_features,
_rowModels: { /* ... */ },
columns,
data,
})
return (
<table.Subscribe
selector={(state) => ({
sorting: state.sorting,
pagination: state.pagination,
})}
>
{({ sorting, pagination }) => (
// This only re-renders when sorting or pagination changes
<div>
<table>{/* ... */}</table>
<div>Page {pagination.pageIndex + 1}</div>
</div>
)}
</table.Subscribe>
)
}
If you want v8-style behavior where the component re-renders on any state change, pass state => state as the selector:
// Re-renders on ANY state change (like v8)
const table = useTable(
{
_features,
_rowModels: { /* ... */ },
columns,
data,
},
(state) => state, // Subscribe to entire state
)
// table.state now contains the full state
const { sorting, pagination, columnFilters } = table.state
Controlled state patterns work similarly to v8:
const [sorting, setSorting] = useState<SortingState>([])
const [pagination, setPagination] = useState<PaginationState>({
pageIndex: 0,
pageSize: 10,
})
const table = useTable({
_features,
_rowModels: { /* ... */ },
columns,
data,
state: {
sorting,
pagination,
},
onSortingChange: setSorting,
onPaginationChange: setPagination,
})
The createColumnHelper function now requires a TFeatures type parameter in addition to TData:
// v8
import { createColumnHelper } from '@tanstack/react-table'
const columnHelper = createColumnHelper<Person>()
// v9
import { createColumnHelper, tableFeatures, rowSortingFeature } from '@tanstack/react-table'
const _features = tableFeatures({ rowSortingFeature })
const columnHelper = createColumnHelper<typeof _features, Person>()
v9 adds a columns() helper for better type inference when wrapping column arrays. In v8, TValue wasn't always type-safe—especially with group columns, where nested column types could be lost or widened. The columns() helper uses variadic tuple types to preserve each column's individual TValue type, so info.getValue() and cell renderers stay correctly typed throughout nested structures:
const columnHelper = createColumnHelper<typeof _features, Person>()
// Wrap your columns array for better type inference
const columns = columnHelper.columns([
columnHelper.accessor('firstName', {
header: 'First Name',
cell: (info) => info.getValue(),
}),
columnHelper.accessor('lastName', {
id: 'lastName',
header: () => <span>Last Name</span>,
cell: (info) => <i>{info.getValue()}</i>,
}),
columnHelper.display({
id: 'actions',
header: 'Actions',
cell: (info) => <button>Edit</button>,
}),
])
When using createTableHook, you get a pre-bound createAppColumnHelper that only requires TData:
const { useAppTable, createAppColumnHelper } = createTableHook({
_features: tableFeatures({ rowSortingFeature }),
_rowModels: { /* ... */ },
})
// TFeatures is already bound — only need TData!
const columnHelper = createAppColumnHelper<Person>()
The flexRender function still exists and works the same way:
import { flexRender } from '@tanstack/react-table'
// Still works in v9
{flexRender(cell.column.columnDef.cell, cell.getContext())}
{flexRender(header.column.columnDef.header, header.getContext())}
v9 adds a cleaner component-based approach attached to the table instance:
const table = useTable({ /* ... */ })
// Instead of:
{flexRender(header.column.columnDef.header, header.getContext())}
// You can use:
<table.FlexRender header={header} />
<table.FlexRender cell={cell} />
<table.FlexRender footer={footer} />
This should be way more convenient and type-safe than the old flexRender function!
There's also a standalone component you can import:
import { FlexRender } from '@tanstack/react-table'
<FlexRender header={header} />
<FlexRender cell={cell} />
<FlexRender footer={footer} />
The tableOptions() helper provides type-safe composition of table options. It's useful for creating reusable partial configurations that can be spread into your table setup.
import { tableOptions, tableFeatures, rowSortingFeature } from '@tanstack/react-table'
// Create a reusable options object with features pre-configured
const baseOptions = tableOptions({
_features: tableFeatures({ rowSortingFeature }),
debugTable: process.env.NODE_ENV === 'development',
})
// Use in your table — columns, data, and other options can be added
const table = useTable({
...baseOptions,
columns,
data,
_rowModels: {},
})
tableOptions() allows you to omit certain required fields (like data, columns, or _features) when creating partial configurations:
// Partial options without data or columns
const featureOptions = tableOptions({
_features: tableFeatures({
rowSortingFeature,
columnFilteringFeature,
}),
_rowModels: {
sortedRowModel: createSortedRowModel(sortFns),
filteredRowModel: createFilteredRowModel(filterFns),
},
})
// Another partial without _features (inherits from spread)
const paginationDefaults = tableOptions({
_rowModels: {
paginatedRowModel: createPaginatedRowModel(),
},
initialState: {
pagination: { pageIndex: 0, pageSize: 25 },
},
})
// Combine them
const table = useTable({
...featureOptions,
...paginationDefaults,
columns,
data,
})
tableOptions() pairs well with createTableHook for building composable table factories:
const sharedOptions = tableOptions({
_features: tableFeatures({ rowSortingFeature, rowPaginationFeature }),
_rowModels: {
sortedRowModel: createSortedRowModel(sortFns),
paginatedRowModel: createPaginatedRowModel(),
},
})
const { useAppTable } = createTableHook(sharedOptions)
This is an advanced, optional feature. You don't need to use createTableHook—useTable is sufficient for most use cases. If you're familiar with TanStack Form's createFormHook, createTableHook works almost the same way: it creates a custom hook with pre-bound configuration that you can reuse across many tables.
For applications with multiple tables sharing the same configuration, createTableHook lets you define features, row models, and reusable components once:
// hooks/table.ts
import {
createTableHook,
tableFeatures,
columnFilteringFeature,
rowSortingFeature,
rowPaginationFeature,
createFilteredRowModel,
createSortedRowModel,
createPaginatedRowModel,
filterFns,
sortFns,
} from '@tanstack/react-table'
// Import your reusable components
import { PaginationControls, SortIndicator, TextCell } from './components'
export const {
useAppTable,
createAppColumnHelper,
useTableContext,
useCellContext,
useHeaderContext,
} = createTableHook({
// Features defined once
_features: tableFeatures({
columnFilteringFeature,
rowSortingFeature,
rowPaginationFeature,
}),
// Row models defined once
_rowModels: {
filteredRowModel: createFilteredRowModel(filterFns),
sortedRowModel: createSortedRowModel(sortFns),
paginatedRowModel: createPaginatedRowModel(),
},
// Default table options
debugTable: process.env.NODE_ENV === 'development',
// Register reusable components
tableComponents: { PaginationControls },
cellComponents: { TextCell },
headerComponents: { SortIndicator },
})
// features/users.tsx
import { useAppTable, createAppColumnHelper } from './hooks/table'
const columnHelper = createAppColumnHelper<Person>()
const columns = columnHelper.columns([
columnHelper.accessor('firstName', {
header: 'First Name',
cell: ({ cell }) => <cell.TextCell />, // Pre-bound component!
}),
])
function UsersTable({ data }: { data: Person[] }) {
const table = useAppTable({
columns,
data,
// _features and _rowModels already configured!
})
return (
<table.AppTable>
<table>
<thead>
{table.getHeaderGroups().map((headerGroup) => (
<tr key={headerGroup.id}>
{headerGroup.headers.map((h) => (
<table.AppHeader header={h} key={h.id}>
{(header) => (
<th>
<header.FlexRender />
<header.SortIndicator />
</th>
)}
</table.AppHeader>
))}
</tr>
))}
</thead>
<tbody>
{table.getRowModel().rows.map((row) => (
<tr key={row.id}>
{row.getAllCells().map((c) => (
<table.AppCell cell={c} key={c.id}>
{(cell) => (
<td>
<cell.FlexRender />
</td>
)}
</table.AppCell>
))}
</tr>
))}
</tbody>
</table>
<table.PaginationControls />
</table.AppTable>
)
}
Components registered via createTableHook can access their context:
// components/SortIndicator.tsx
import { useHeaderContext } from './hooks/table'
export function SortIndicator() {
const header = useHeaderContext()
const sorted = header.column.getIsSorted()
if (!sorted) return null
return sorted === 'asc' ? ' 🔼' : ' 🔽'
}
// components/TextCell.tsx
import { useCellContext } from './hooks/table'
export function TextCell() {
const cell = useCellContext()
return <span>{cell.getValue() as string}</span>
}
// components/PaginationControls.tsx
import { useTableContext } from './hooks/table'
export function PaginationControls() {
const table = useTableContext()
return (
<table.Subscribe selector={(s) => s.pagination}>
{(pagination) => (
<div>
<button onClick={() => table.previousPage()}>Previous</button>
<span>Page {pagination.pageIndex + 1}</span>
<button onClick={() => table.nextPage()}>Next</button>
</div>
)}
</table.Subscribe>
)
}
The enablePinning option has been split into separate options:
// v8
enablePinning: true
// v9
enableColumnPinning: true
enableRowPinning: true
All internal APIs prefixed with _ have been removed. If you were using any of these, use their public equivalents:
In v8, column sizing and resizing were combined in a single feature. In v9, they've been split into separate features for better tree-shaking.
| v8 | v9 |
|---|---|
| ColumnSizing (combined feature) | columnSizingFeature + columnResizingFeature |
| columnSizingInfo state | columnResizing state |
| setColumnSizingInfo() | setColumnResizing() |
| onColumnSizingInfoChange option | onColumnResizingChange option |
If you only need column sizing (fixed widths) without interactive resizing, you can import just columnSizingFeature. If you need drag-to-resize functionality, import both:
import { columnSizingFeature, columnResizingFeature } from '@tanstack/react-table'
const _features = tableFeatures({
columnSizingFeature,
columnResizingFeature, // Only if you need interactive resizing
})
Sorting-related APIs have been renamed for consistency:
| v8 | v9 |
|---|---|
| sortingFn (column def option) | sortFn |
| column.getSortingFn() | column.getSortFn() |
| column.getAutoSortingFn() | column.getAutoSortFn() |
| SortingFn type | SortFn type |
| SortingFns interface | SortFns interface |
| sortingFns (built-in functions) | sortFns |
Update your column definitions:
// v8
const columns = [
{
accessorKey: 'name',
sortingFn: 'alphanumeric', // or custom function
},
]
// v9
const columns = [
{
accessorKey: 'name',
sortFn: 'alphanumeric', // or custom function
},
]
Some row APIs have changed from private to public:
| v8 | v9 |
|---|---|
| row._getAllCellsByColumnId() (private) | row.getAllCellsByColumnId() (public) |
If you were accessing this internal API, you can now use it without the underscore prefix.
Most types now require a TFeatures parameter:
// v8
type Column<TData>
type ColumnDef<TData>
type Table<TData>
type Row<TData>
type Cell<TData, TValue>
// v9
type Column<TFeatures, TData, TValue>
type ColumnDef<TFeatures, TData, TValue>
type Table<TFeatures, TData>
type Row<TFeatures, TData>
type Cell<TFeatures, TData, TValue>
The easiest way to get the TFeatures type is with typeof:
const _features = tableFeatures({
rowSortingFeature,
columnFilteringFeature,
})
// Use typeof to get the type
type MyFeatures = typeof _features
const columns: ColumnDef<typeof _features, Person>[] = [...]
function Filter({ column }: { column: Column<typeof _features, Person, unknown> }) {
// ...
}
If using stockFeatures with useTable, use the StockFeatures type:
import type { StockFeatures, ColumnDef } from '@tanstack/react-table'
const columns: ColumnDef<StockFeatures, Person>[] = [...]
If you're using module augmentation to extend ColumnMeta, note that it now requires a TFeatures parameter:
// v8
declare module '@tanstack/react-table' {
interface ColumnMeta<TData, TValue> {
customProperty: string
}
}
// v9 - TFeatures is now the first parameter
declare module '@tanstack/react-table' {
interface ColumnMeta<TFeatures, TData, TValue> {
customProperty: string
}
}
The RowData type is now more restrictive:
// v8 - very permissive
type RowData = unknown | object | any[]
// v9 - must be a record or array
type RowData = Record<string, any> | Array<any>
This change improves type safety. If you were passing unusual data types, ensure your data conforms to Record<string, any> or Array<any>.
Check out these examples to see v9 patterns in action: