| name | react/getting-started |
| description | End-to-end first-table journey for `@tanstack/react-table` v9. Install the React adapter, declare `features` via `tableFeatures()`, declare `rowModels` factories with their *Fns parameters (`createSortedRowModel(sortFns)` etc.), create a column helper with both `TFeatures` and `TData` generics, instantiate `useTable`, and render with `<table.FlexRender>`. New users land here, not on `useLegacyTable`.
|
| type | lifecycle |
| library | tanstack-table |
| framework | react |
| library_version | 9.0.0-alpha.48 |
| requires | ["setup","column-definitions","state-management","react/table-state"] |
| sources | ["TanStack/table:docs/installation.md","TanStack/table:docs/framework/react/react-table.md","TanStack/table:examples/react/basic-use-table/src/main.tsx","TanStack/table:examples/react/basic-use-app-table/src/main.tsx"] |
This skill builds on tanstack-table/state-management and tanstack-table/react/table-state. Read those first — features + rowModels come from the core state-management concept, and table-state covers how reactivity flows in React.
Install
pnpm add @tanstack/react-table
npm install @tanstack/react-table
@tanstack/react-table v9 requires React 18+ and TypeScript 5.4+ if you use TS.
Minimum-viable v9 table
Three things are non-negotiable, even for the simplest possible table:
features: tableFeatures({...}) — required even if empty (tableFeatures({})).
rowModels: {...} — required even if empty (rowModels: {}). The core row model is automatic; you only register sorted/filtered/paginated/grouped/etc. when you use them.
createColumnHelper<typeof features, TData>() — generic order is <TFeatures, TData> in v9 (changed from v8).
import * as React from 'react'
import { useTable, tableFeatures } from '@tanstack/react-table'
import type { ColumnDef } from '@tanstack/react-table'
type Person = {
firstName: string
lastName: string
age: number
visits: number
status: string
progress: number
}
const features = tableFeatures({})
const columns: Array<ColumnDef<typeof features, Person>> = [
{
accessorKey: 'firstName',
header: 'First Name',
cell: (info) => info.getValue(),
},
{ accessorKey: 'lastName', header: 'Last Name' },
{ accessorKey: 'age', header: 'Age' },
{ accessorKey: 'visits', header: 'Visits' },
{ accessorKey: 'status', header: 'Status' },
{ accessorKey: 'progress', header: 'Profile Progress' },
]
function App({ initialData }: { initialData: Person[] }) {
const [data] = React.useState(() => initialData)
const table = useTable(
{
features,
rowModels: {},
columns,
data,
},
(state) => state,
)
return (
<table>
<thead>
{table.getHeaderGroups().map((hg) => (
<tr key={hg.id}>
{hg.headers.map((h) => (
<th key={h.id}>
{h.isPlaceholder ? null : <table.FlexRender header={h} />}
</th>
))}
</tr>
))}
</thead>
<tbody>
{table.getRowModel().rows.map((row) => (
<tr key={row.id}>
{row.getAllCells().map((cell) => (
<td key={cell.id}>
<table.FlexRender cell={cell} />
</td>
))}
</tr>
))}
</tbody>
</table>
)
}
Source: examples/react/basic-use-table/src/main.tsx.
Adding sorting
Register the feature in features, the factory in rowModels, and wire a click handler:
import {
useTable,
tableFeatures,
rowSortingFeature,
createSortedRowModel,
sortFns,
createColumnHelper,
} from '@tanstack/react-table'
const features = tableFeatures({ rowSortingFeature })
const columnHelper = createColumnHelper<typeof features, Person>()
const columns = columnHelper.columns([
columnHelper.accessor('firstName', { header: 'First' }),
columnHelper.accessor('age', { header: 'Age' }),
])
function App({ data }: { data: Person[] }) {
const table = useTable({
features,
rowModels: { sortedRowModel: createSortedRowModel(sortFns) },
columns,
data,
})
return (
<table>
<thead>
{table.getHeaderGroups().map((hg) => (
<tr key={hg.id}>
{hg.headers.map((h) => (
<th key={h.id} onClick={h.column.getToggleSortingHandler()}>
<table.FlexRender header={h} />
{{ asc: ' 🔼', desc: ' 🔽' }[
h.column.getIsSorted() as string
] ?? null}
</th>
))}
</tr>
))}
</thead>
{/* tbody same as above */}
</table>
)
}
createSortedRowModel REQUIRES sortFns (and the equivalents for createFilteredRowModel(filterFns), createGroupedRowModel(aggregationFns)). The factory parameter is what makes the registry tree-shakeable.
Layering features
Adding pagination and filtering is purely additive — register the feature + factory, and call the built-in APIs:
const features = tableFeatures({
rowSortingFeature,
rowPaginationFeature,
columnFilteringFeature,
})
const table = useTable({
features,
rowModels: {
sortedRowModel: createSortedRowModel(sortFns),
filteredRowModel: createFilteredRowModel(filterFns),
paginatedRowModel: createPaginatedRowModel(),
},
columns,
data,
})
table.setSorting([{ id: 'age', desc: true }])
table.nextPage()
table.setColumnFilters([{ id: 'firstName', value: 'tan' }])
column.toggleSorting()
row.toggleSelected()
Source: docs/framework/react/react-table.md; examples/react/basic-use-table/src/main.tsx.
Optional: createTableHook for shared config
If you ship the same features / rowModels / cell components across many tables, package them once:
import { createTableHook } from '@tanstack/react-table'
const { useAppTable, createAppColumnHelper } = createTableHook({
features: {},
rowModels: {},
debugTable: true,
})
const columnHelper = createAppColumnHelper<Person>()
const columns = columnHelper.columns([
columnHelper.accessor('firstName', { cell: (info) => info.getValue() }),
])
function App({ data }) {
const table = useAppTable({ columns, data })
}
Source: examples/react/basic-use-app-table/src/main.tsx.
Common Mistakes
CRITICAL Forgetting features: tableFeatures({})
Wrong:
const table = useTable({
rowModels: {},
columns,
data,
})
Correct:
const features = tableFeatures({})
const table = useTable({ features, rowModels: {}, columns, data })
The option is required even for a "no features" table — pass tableFeatures({}) or stockFeatures if you want v8-style "everything on".
Source: examples/react/basic-use-table/src/main.tsx.
CRITICAL Reimplementing what built-in APIs already provide
Wrong:
const [sorting, setSorting] = useState([])
const sortedData = useMemo(
() => [...data].sort((a, b) => ),
[data, sorting],
)
Correct:
const table = useTable({
features: tableFeatures({ rowSortingFeature }),
rowModels: { sortedRowModel: createSortedRowModel(sortFns) },
columns,
data,
})
Maintainer flags this as the #1 tell that "an AI wrote this." The built-ins handle reset semantics, multi-sort, internal invariants.
Source: maintainer interview (Phase 4).
CRITICAL API "missing" because the feature was not registered in features
Wrong:
const features = tableFeatures({})
const table = useTable({ features, rowModels: {}, columns, data })
table.setSorting([{ id: 'age', desc: true }])
Correct:
const features = tableFeatures({ rowSortingFeature })
const table = useTable({
features,
rowModels: { sortedRowModel: createSortedRowModel(sortFns) },
columns,
data,
})
table.setSorting([{ id: 'age', desc: true }])
In v9, features is a tree-shakeable registry. If a feature isn't listed, TypeScript hides its APIs and the runtime atom is never created — the feature isn't broken, it's just not on.
Source: maintainer interview (Phase 4); docs/framework/react/react-table.md.
HIGH Wrong generic order on createColumnHelper
Wrong:
const columnHelper = createColumnHelper<Person>()
Correct:
const features = tableFeatures({
})
const columnHelper = createColumnHelper<typeof features, Person>()
v9 added TFeatures as the first generic across Column, Row, ColumnDef, ColumnMeta, etc. Use typeof features so the same feature set drives types and runtime.
Source: docs/framework/react/react-table.md.
HIGH Defining features / columns / data inside the render body
Wrong:
function MyTable({ rows }) {
const features = tableFeatures({ rowSortingFeature })
const columns = []
return <Table {/* … */} />
}
Correct:
const features = tableFeatures({ rowSortingFeature })
const columns: ColumnDef<typeof features, Person>[] = [
]
Internal memoization keys off identity. A new object every render forces full recomputation and can cause subtle re-render issues.
Source: examples/react/basic-use-table/src/main.tsx; FAQ #1.
HIGH Reaching for useLegacyTable for a new project
Wrong:
import { useLegacyTable, getCoreRowModel } from '@tanstack/react-table/legacy'
const table = useLegacyTable({
columns,
data,
getCoreRowModel: getCoreRowModel(),
})
Correct:
import { useTable, tableFeatures } from '@tanstack/react-table'
const features = tableFeatures({})
const table = useTable({ features, rowModels: {}, columns, data })
useLegacyTable is a migration shim for incrementally upgrading v8 codebases. It bundles every feature, lacks table.Subscribe, and is deprecated in v9 / scheduled for removal in v10. New code uses useTable.
Source: docs/framework/react/guide/use-legacy-table.md.
See Also
tanstack-table/react/table-state — selectors, <Subscribe>, external atoms, createTableHook.
tanstack-table/react/migrate-v8-to-v9 — for codebases upgrading from useReactTable.
tanstack-table/react/production-readiness — once it works, optimize for shipping.
tanstack-table/react/client-to-server — when you outgrow client-side row processing.