v9.0.0-beta.10 introduces a breaking change in how row models are defined in order to bring increased type-safety features. Row model factories and function registries now live as slots on the features object instead of a separate rowModels option, and the factories no longer take arguments. If you migrated on an earlier beta, see the Row Model Factories section below for the new shape.
TanStack Table v9 is a major release with a smaller, more explicit table setup. The core table logic is familiar, but the table instance now declares exactly which features, row models, and state subscriptions it needs.
The main migration is changing from the React adapter used through preact/compat to the native Preact adapter: useReactTable becomes useTable, and get*RowModel options become feature and row model factory slots on tableFeatures.
TanStack Table v8 did not have an officially released Preact adapter. If you used TanStack Table in a Preact app on v8, you were most likely using @tanstack/react-table through preact/compat.
This guide is for migrating that setup to the native v9 @tanstack/preact-table adapter. After this migration, TanStack Table's Preact packages should not be the reason your table code requires preact/compat; any remaining compat aliases should come from the rest of your app or other dependencies.
// v8 / before: Preact app using the React adapter through preact/compat
import { useReactTable } from '@tanstack/react-table'
const table = useReactTable(options)
// v9: native Preact adapter
import { useTable } from '@tanstack/preact-table'
const table = useTable(options)// v8 / before: Preact app using the React adapter through preact/compat
import { useReactTable } from '@tanstack/react-table'
const table = useReactTable(options)
// v9: native Preact adapter
import { useTable } from '@tanstack/preact-table'
const table = useTable(options)In v9, a table must declare its feature set. Row model factories are registered as slots on tableFeatures rather than as a separate rowModels option.
// v8 / before: React adapter through preact/compat
import { getCoreRowModel, useReactTable } from '@tanstack/react-table'
const table = useReactTable({
columns,
data,
getCoreRowModel: getCoreRowModel(),
})
// v9
import { tableFeatures, useTable } from '@tanstack/preact-table'
const features = tableFeatures({}) // core row model is automatic
const table = useTable({
features,
columns,
data,
})// v8 / before: React adapter through preact/compat
import { getCoreRowModel, useReactTable } from '@tanstack/react-table'
const table = useReactTable({
columns,
data,
getCoreRowModel: getCoreRowModel(),
})
// v9
import { tableFeatures, useTable } from '@tanstack/preact-table'
const features = tableFeatures({}) // core row model is automatic
const table = useTable({
features,
columns,
data,
})Keep the features object outside the component when possible so the reference stays stable.
Features control which APIs, options, and state slices exist on the table instance. In the v8 React adapter, features were bundled together. In v9, importing and registering only what you use is the default.
import {
columnFilteringFeature,
columnVisibilityFeature,
rowPaginationFeature,
rowSelectionFeature,
rowSortingFeature,
tableFeatures,
} from '@tanstack/preact-table'
const features = tableFeatures({
columnFilteringFeature,
columnVisibilityFeature,
rowPaginationFeature,
rowSelectionFeature,
rowSortingFeature,
})import {
columnFilteringFeature,
columnVisibilityFeature,
rowPaginationFeature,
rowSelectionFeature,
rowSortingFeature,
tableFeatures,
} from '@tanstack/preact-table'
const features = tableFeatures({
columnFilteringFeature,
columnVisibilityFeature,
rowPaginationFeature,
rowSelectionFeature,
rowSortingFeature,
})stockFeatures includes the common feature set and can be useful for smoke tests or early migration. It gives up the main bundle-size benefit of v9, so audit it before shipping.
import { stockFeatures, useTable } from '@tanstack/preact-table'
const table = useTable({
features: stockFeatures,
columns,
data,
})import { stockFeatures, useTable } from '@tanstack/preact-table'
const table = useTable({
features: stockFeatures,
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 process data for features like filtering, sorting, grouping, expanding, faceting, and pagination. In v9, row model factories and function registries are slots on tableFeatures rather than a separate rowModels option.
| v8 Option | v9 tableFeatures Slot | v9 Factory |
|---|---|---|
| getCoreRowModel() | (automatic) | Not needed |
| getFilteredRowModel() + filterFns | filteredRowModel + filterFns | createFilteredRowModel() |
| getSortedRowModel() + sortingFns | sortedRowModel + sortFns | createSortedRowModel() |
| getPaginationRowModel() | paginatedRowModel | createPaginatedRowModel() |
| getExpandedRowModel() | expandedRowModel | createExpandedRowModel() |
| getGroupedRowModel() + aggregationFns | groupedRowModel + aggregationFns | createGroupedRowModel() |
| getFacetedRowModel() | facetedRowModel | createFacetedRowModel() |
| getFacetedMinMaxValues() | facetedMinMaxValues | createFacetedMinMaxValues() |
| getFacetedUniqueValues() | facetedUniqueValues | createFacetedUniqueValues() |
// v8 / before: React adapter through preact/compat
import {
getCoreRowModel,
getFilteredRowModel,
getPaginationRowModel,
getSortedRowModel,
sortingFns,
filterFns,
useReactTable,
} from '@tanstack/react-table'
const table = useReactTable({
columns,
data,
getCoreRowModel: getCoreRowModel(),
getFilteredRowModel: getFilteredRowModel(),
getSortedRowModel: getSortedRowModel(),
getPaginationRowModel: getPaginationRowModel(),
sortingFns,
filterFns,
})
// v9
import {
columnFilteringFeature,
createFilteredRowModel,
createPaginatedRowModel,
createSortedRowModel,
filterFns,
rowPaginationFeature,
rowSortingFeature,
sortFns,
tableFeatures,
useTable,
} from '@tanstack/preact-table'
const features = tableFeatures({
columnFilteringFeature,
rowPaginationFeature,
rowSortingFeature,
filteredRowModel: createFilteredRowModel(),
sortedRowModel: createSortedRowModel(),
paginatedRowModel: createPaginatedRowModel(),
filterFns,
sortFns,
})
const table = useTable({
features,
columns,
data,
})// v8 / before: React adapter through preact/compat
import {
getCoreRowModel,
getFilteredRowModel,
getPaginationRowModel,
getSortedRowModel,
sortingFns,
filterFns,
useReactTable,
} from '@tanstack/react-table'
const table = useReactTable({
columns,
data,
getCoreRowModel: getCoreRowModel(),
getFilteredRowModel: getFilteredRowModel(),
getSortedRowModel: getSortedRowModel(),
getPaginationRowModel: getPaginationRowModel(),
sortingFns,
filterFns,
})
// v9
import {
columnFilteringFeature,
createFilteredRowModel,
createPaginatedRowModel,
createSortedRowModel,
filterFns,
rowPaginationFeature,
rowSortingFeature,
sortFns,
tableFeatures,
useTable,
} from '@tanstack/preact-table'
const features = tableFeatures({
columnFilteringFeature,
rowPaginationFeature,
rowSortingFeature,
filteredRowModel: createFilteredRowModel(),
sortedRowModel: createSortedRowModel(),
paginatedRowModel: createPaginatedRowModel(),
filterFns,
sortFns,
})
const table = useTable({
features,
columns,
data,
})In v8 React-adapter examples, most code read all state through table.getState(). In v9, Preact can read a full snapshot, selected state, or a single atom.
| Surface | Use |
|---|---|
| table.state | The selected state from useTable; by default, this is the full registered table state. |
| table.store.state | A full framework-agnostic table state snapshot. |
| table.atoms.<slice>.get() | A narrow current-value read for one state slice. |
| table.Subscribe | A render boundary for selected table state or a specific atom/store source. |
| table.baseAtoms.<slice> | Internal writable atoms. Prefer feature APIs instead of writing these directly. |
// v8
const sorting = table.getState().sorting
const pagination = table.getState().pagination
// v9: full snapshot
const sorting = table.store.state.sorting
const pagination = table.store.state.pagination
// v9: narrow atom read
const sorting = table.atoms.sorting.get()// v8
const sorting = table.getState().sorting
const pagination = table.getState().pagination
// v9: full snapshot
const sorting = table.store.state.sorting
const pagination = table.store.state.pagination
// v9: narrow atom read
const sorting = table.atoms.sorting.get()By default, table.state is reactive and contains the full registered table state:
const table = useTable({
features,
columns,
data,
})
const { pagination, sorting } = table.stateconst table = useTable({
features,
columns,
data,
})
const { pagination, sorting } = table.statePass a custom selector when you want table.state to contain only the reactive state values that should cause this component to re-render.
const table = useTable(
{
features,
columns,
data,
},
(state) => ({
pagination: state.pagination,
sorting: state.sorting,
}),
)
table.state.paginationconst table = useTable(
{
features,
columns,
data,
},
(state) => ({
pagination: state.pagination,
sorting: state.sorting,
}),
)
table.state.paginationPassing (state) => state is equivalent to the default selector and is no longer necessary.
For large tables, opt the parent out and subscribe lower in the tree:
const table = useTable({ features, columns, data }, () => null)const table = useTable({ features, columns, data }, () => null)function PaginationFooter({ table }) {
return (
<table.Subscribe
selector={(state) => ({
pagination: state.pagination,
})}
>
{({ pagination }) => (
<span>Page {pagination.pageIndex + 1}</span>
)}
</table.Subscribe>
)
}function PaginationFooter({ table }) {
return (
<table.Subscribe
selector={(state) => ({
pagination: state.pagination,
})}
>
{({ pagination }) => (
<span>Page {pagination.pageIndex + 1}</span>
)}
</table.Subscribe>
)
}table.Subscribe can also subscribe directly to one atom:
<table.Subscribe source={table.atoms.rowSelection}>
{(rowSelection) => <span>{Object.keys(rowSelection).length} selected</span>}
</table.Subscribe><table.Subscribe source={table.atoms.rowSelection}>
{(rowSelection) => <span>{Object.keys(rowSelection).length} selected</span>}
</table.Subscribe>The v8-style state plus on[State]Change pattern still works for migration and remains convenient for simple integrations. Keep it per-slice. For new v9 code, prefer owning state slices with external atoms (see External Atoms below), which give you fine-grained subscriptions without mirroring state through Preact.
import { useState } from 'preact/hooks'
import type { PaginationState, SortingState } from '@tanstack/preact-table'
const [sorting, setSorting] = useState<SortingState>([])
const [pagination, setPagination] = useState<PaginationState>({
pageIndex: 0,
pageSize: 10,
})
const table = useTable({
features,
columns,
data,
state: {
sorting,
pagination,
},
onSortingChange: setSorting,
onPaginationChange: setPagination,
})import { useState } from 'preact/hooks'
import type { PaginationState, SortingState } from '@tanstack/preact-table'
const [sorting, setSorting] = useState<SortingState>([])
const [pagination, setPagination] = useState<PaginationState>({
pageIndex: 0,
pageSize: 10,
})
const table = useTable({
features,
columns,
data,
state: {
sorting,
pagination,
},
onSortingChange: setSorting,
onPaginationChange: setPagination,
})The v8-style onStateChange callback is no longer part of the v9 useTable state model.
Use external atoms when the app should own a table state slice and share it outside the table.
import { useCreateAtom, useSelector } from '@tanstack/preact-store'
import type { PaginationState, SortingState } from '@tanstack/preact-table'
function MyTable({ columns, data }) {
const sortingAtom = useCreateAtom<SortingState>([])
const paginationAtom = useCreateAtom<PaginationState>({
pageIndex: 0,
pageSize: 10,
})
const sorting = useSelector(sortingAtom)
const pagination = useSelector(paginationAtom)
const table = useTable({
features,
columns,
data,
atoms: {
sorting: sortingAtom,
pagination: paginationAtom,
},
})
return <span>Page {pagination.pageIndex + 1}</span>
}import { useCreateAtom, useSelector } from '@tanstack/preact-store'
import type { PaginationState, SortingState } from '@tanstack/preact-table'
function MyTable({ columns, data }) {
const sortingAtom = useCreateAtom<SortingState>([])
const paginationAtom = useCreateAtom<PaginationState>({
pageIndex: 0,
pageSize: 10,
})
const sorting = useSelector(sortingAtom)
const pagination = useSelector(paginationAtom)
const table = useTable({
features,
columns,
data,
atoms: {
sorting: sortingAtom,
pagination: paginationAtom,
},
})
return <span>Page {pagination.pageIndex + 1}</span>
}When atoms.pagination is provided, table writes like table.setPageIndex(2) write to that atom. Do not also pass state.pagination; atoms take precedence.
TFeatures is now the first generic for column helpers and table types.
// v8
const columnHelper = createColumnHelper<Person>()
const columns: ColumnDef<Person>[] = [
columnHelper.accessor('age', {
header: 'Age',
sortingFn: 'alphanumeric',
}),
]
// v9
const columnHelper = createColumnHelper<typeof features, Person>()
const columns: Array<ColumnDef<typeof features, Person>> = columnHelper.columns([
columnHelper.accessor('age', {
header: 'Age',
sortFn: 'alphanumeric',
}),
])// v8
const columnHelper = createColumnHelper<Person>()
const columns: ColumnDef<Person>[] = [
columnHelper.accessor('age', {
header: 'Age',
sortingFn: 'alphanumeric',
}),
]
// v9
const columnHelper = createColumnHelper<typeof features, Person>()
const columns: Array<ColumnDef<typeof features, Person>> = columnHelper.columns([
columnHelper.accessor('age', {
header: 'Age',
sortFn: 'alphanumeric',
}),
])Use columnHelper.columns([...]) to preserve better inference for nested and grouped column definitions.
The React-adapter flexRender(def, context) function still exists for advanced cases, but v9 prefers the table-aware FlexRender component.
// v8
<td>{flexRender(cell.column.columnDef.cell, cell.getContext())}</td>
// v9
<td><table.FlexRender cell={cell} /></td>// v8
<td>{flexRender(cell.column.columnDef.cell, cell.getContext())}</td>
// v9
<td><table.FlexRender cell={cell} /></td>You can also import the standalone component:
import { FlexRender } from '@tanstack/preact-table'
<FlexRender header={header} />
<FlexRender cell={cell} />
<FlexRender footer={header} />import { FlexRender } from '@tanstack/preact-table'
<FlexRender header={header} />
<FlexRender cell={cell} />
<FlexRender footer={header} />tableOptions() is a type helper for reusable table option fragments.
import { tableOptions } from '@tanstack/preact-table'
const baseOptions = tableOptions({
features,
defaultColumn: {
minSize: 40,
},
})
const table = useTable({
...baseOptions,
columns,
data,
})import { tableOptions } from '@tanstack/preact-table'
const baseOptions = tableOptions({
features,
defaultColumn: {
minSize: 40,
},
})
const table = useTable({
...baseOptions,
columns,
data,
})Use it when several tables share feature registration, row models, defaults, or manual server-side settings.
createTableHook creates app-specific Preact table helpers with features, row models, and component conventions already bound.
import { createTableHook } from '@tanstack/preact-table'
const { useAppTable, createAppColumnHelper } = createTableHook({ features })
const columnHelper = createAppColumnHelper<Person>()
function PeopleTable({ data }) {
const table = useAppTable({
columns,
data,
})
}import { createTableHook } from '@tanstack/preact-table'
const { useAppTable, createAppColumnHelper } = createTableHook({ features })
const columnHelper = createAppColumnHelper<Person>()
function PeopleTable({ data }) {
const table = useAppTable({
columns,
data,
})
}See the Composable Tables Guide for full patterns.
At the table level, enablePinning split into column and row options:
const table = useTable({
enableColumnPinning: true,
enableRowPinning: true,
})const table = useTable({
enableColumnPinning: true,
enableRowPinning: true,
})Per-column enablePinning remains a column option.
Column resizing now has its own feature and state slice.
const features = tableFeatures({
columnSizingFeature,
columnResizingFeature,
})const features = tableFeatures({
columnSizingFeature,
columnResizingFeature,
})columnSizingInfo is now columnResizing, and onColumnSizingInfoChange is now onColumnResizingChange.
| v8 | v9 |
|---|---|
| sortingFn | sortFn |
| sortingFns | sortFns |
| getSortingFn() | getSortFn() |
| getAutoSortingFn() | getAutoSortFn() |
| SortingFn | SortFn |
| SortingFns | SortFns |
Several underscore-prefixed APIs are now public without the underscore. For example, row._getAllCellsByColumnId() becomes row.getAllCellsByColumnId().
TFeatures is now the first generic on core table types.
ColumnDef<typeof features, Person>
Column<typeof features, Person>
Row<typeof features, Person>
Cell<typeof features, Person, TValue>
Table<typeof features, Person>ColumnDef<typeof features, Person>
Column<typeof features, Person>
Row<typeof features, Person>
Cell<typeof features, Person, TValue>
Table<typeof features, Person>Use the concrete features object for type inference:
const features = tableFeatures({
rowSortingFeature,
rowPaginationFeature,
})
const columnHelper = createColumnHelper<typeof features, Person>()const features = tableFeatures({
rowSortingFeature,
rowPaginationFeature,
})
const columnHelper = createColumnHelper<typeof features, Person>()If a helper must support stockFeatures, use StockFeatures:
import type { StockFeatures } from '@tanstack/preact-table'
type PersonColumn = ColumnDef<StockFeatures, Person>import type { StockFeatures } from '@tanstack/preact-table'
type PersonColumn = ColumnDef<StockFeatures, Person>No more declaration merging required! (Although it still works if you want to keep using it)
Global declaration merging works exactly like it did in v8. The only change you need to make is updating the generics shape: both interfaces now take TFeatures as the first type parameter.
declare module '@tanstack/preact-table' {
interface ColumnMeta<TFeatures, TData, TValue> {
align?: 'left' | 'right'
}
}declare module '@tanstack/preact-table' {
interface ColumnMeta<TFeatures, TData, TValue> {
align?: 'left' | 'right'
}
}That's all that's required if you want to keep declaring meta types globally.
Optionally, v9 also adds a new way to declare meta types per-table without declaration merging. You can use type-only tableMeta/columnMeta slots on the features option, which only affect tables created with that features object:
const features = tableFeatures({
rowSortingFeature,
columnMeta: metaHelper<{ align?: 'left' | 'right' }>(),
})const features = tableFeatures({
rowSortingFeature,
columnMeta: metaHelper<{ align?: 'left' | 'right' }>(),
})See the new Table and Column Meta Guide for full details on both approaches.
In v8, making a custom function usable as a string reference (like filterFn: 'fuzzy') required declare module augmentation of the FilterFns interface, and typing filter meta required augmenting FilterMeta. In v9, registering the function in the matching registry slot does both jobs with no global augmentation:
// v8 / before: React adapter through preact/compat
declare module '@tanstack/react-table' {
interface FilterFns {
fuzzy: FilterFn<unknown>
}
interface FilterMeta {
itemRank: RankingInfo
}
}
// v9 - register in the slot; the key becomes a valid string value
interface FuzzyFilterMeta {
itemRank?: RankingInfo
}
const features = tableFeatures({
columnFilteringFeature,
filteredRowModel: createFilteredRowModel(),
filterFns: { ...filterFns, fuzzy: fuzzyFilter },
filterMeta: metaHelper<FuzzyFilterMeta>(),
})
// 'fuzzy' now typechecks in column defs for tables using these features
columnHelper.accessor('name', { filterFn: 'fuzzy' })// v8 / before: React adapter through preact/compat
declare module '@tanstack/react-table' {
interface FilterFns {
fuzzy: FilterFn<unknown>
}
interface FilterMeta {
itemRank: RankingInfo
}
}
// v9 - register in the slot; the key becomes a valid string value
interface FuzzyFilterMeta {
itemRank?: RankingInfo
}
const features = tableFeatures({
columnFilteringFeature,
filteredRowModel: createFilteredRowModel(),
filterFns: { ...filterFns, fuzzy: fuzzyFilter },
filterMeta: metaHelper<FuzzyFilterMeta>(),
})
// 'fuzzy' now typechecks in column defs for tables using these features
columnHelper.accessor('name', { filterFn: 'fuzzy' })The same pattern applies to sortFns (for sortFn string values) and aggregationFns (for aggregationFn string values). See the Fuzzy Filtering Guide for a complete example.
RowData is now constrained to record-like objects or arrays. Prefer object row types such as:
type Person = {
firstName: string
lastName: string
age: number
}type Person = {
firstName: string
lastName: string
age: number
}