TanStack Table
Version: @tanstack/react-table@latest
Requires: React 16.8+, TypeScript recommended
Quick Setup
npm install @tanstack/react-table
import {
useReactTable,
getCoreRowModel,
flexRender,
createColumnHelper,
} from '@tanstack/react-table'
type User = {
name: string
age: number
status: string
}
const columnHelper = createColumnHelper<User>()
const columns = [
columnHelper.accessor('name', { header: 'Name' }),
columnHelper.accessor('age', { header: 'Age' }),
columnHelper.accessor('status', { header: 'Status' }),
columnHelper.display({
id: 'actions',
cell: (props) => <button => edit(props.row.original)}>Edit</button>,
}),
]
function App() {
const [data] = useState<User[]>([]) // must be stable reference
const table = useReactTable({
data,
columns,
getCoreRowModel: getCoreRowModel(),
})
return (
<table>
<thead>
{table.getHeaderGroups().map((headerGroup) => (
<tr key={headerGroup.id}>
{headerGroup.headers.map((header) => (
<th key={header.id}>
{flexRender(header.column.columnDef.header, header.getContext())}
</th>
))}
</tr>
))}
</thead>
<tbody>
{table.getRowModel().rows.map((row) => (
<tr key={row.id}>
{row.getVisibleCells().map((cell) => (
<td key={cell.id}>
{flexRender(cell.column.columnDef.cell, cell.getContext())}
</td>
))}
</tr>
))}
</tbody>
</table>
)
}
Row Models (Import Only What You Need)
TanStack Table is modular. Only import the row models you actually use:
import {
getCoreRowModel, // required
getSortedRowModel, // client-side sorting
getFilteredRowModel, // client-side filtering
getPaginationRowModel,// client-side pagination
getExpandedRowModel, // expanding/sub-rows
getGroupedRowModel, // grouping + aggregation
getFacetedRowModel, // faceted values
getFacetedUniqueValues,
getFacetedMinMaxValues,
} from '@tanstack/react-table'
Pipeline order: Core -> Filtered -> Grouped -> Sorted -> Expanded -> Paginated -> Rendered
Rule Categories
| Priority |
Category |
Rule File |
Impact |
| CRITICAL |
Table Setup |
rules/table-setup.md |
Correct table creation, stable data references |
| CRITICAL |
Column Definitions |
rules/col-column-defs.md |
Data model, rendering, type safety |
| CRITICAL |
Row Models |
rules/rm-row-models.md |
Modular imports, pipeline order |
| HIGH |
Sorting |
rules/sort-sorting.md |
Client/server sorting, custom sort functions |
| HIGH |
Column Filtering |
rules/filt-column-filtering.md |
Per-column filters, custom filter functions |
| HIGH |
Global Filtering |
rules/filt-global-filtering.md |
Table-wide search, global filter function |
| HIGH |
Pagination |
rules/pag-pagination.md |
Client/server pagination, page state |
| MEDIUM |
Row Selection |
rules/sel-row-selection.md |
Checkbox/radio selection, selection state |
| MEDIUM |
Column Visibility |
rules/vis-column-visibility.md |
Show/hide columns dynamically |
| MEDIUM |
Column Sizing |
rules/size-column-sizing.md |
Widths, resizing, performance |
| LOW |
Expanding |
rules/exp-expanding.md |
Sub-rows, detail panels, hierarchical data |
Critical Rules
Always Do
- Stable
data reference — use useState, useMemo, or define outside component to prevent infinite re-renders
- Use
createColumnHelper<TData>() — for maximum type safety in column definitions
- Import only needed row models — don't import
getSortedRowModel if you don't sort client-side
- Use
flexRender — for rendering header/cell/footer templates from column defs
- Use
getVisibleCells() — not getAllCells(), to respect column visibility
- Use
getRowModel() — the final row model that applies all features (filtering, sorting, pagination)
- Control state with
state + on*Change — for sorting, filtering, pagination, selection, etc.
Never Do
- Define
data inline — useReactTable({ data: fetchData() }) causes infinite re-renders
- Define
columns inside render — columns array must be stable (define outside component or useMemo)
- Use
getAllCells() for rendering — ignores column visibility; use getVisibleCells()
- Mix
initialState and state for the same feature — state overrides initialState
- Use client-side row models with
manual* options — if manualSorting: true, don't import getSortedRowModel
- Forget
getRowId — without it, row IDs default to index, breaking selection state across re-fetches
Key Patterns
// Controlled sorting state
const [sorting, setSorting] = useState<SortingState>([])
const table = useReactTable({
data, columns,
getCoreRowModel: getCoreRowModel(),
getSortedRowModel: getSortedRowModel(),
state: { sorting },
onSortingChange: setSorting,
})
// Header click handler
<th
{flexRender(header.column.columnDef.header, header.getContext())}
{{ asc: ' 🔼', desc: ' 🔽' }[header.column.getIsSorted() as string] ?? ''}
</th>
// Server-side pagination
const [pagination, setPagination] = useState({ pageIndex: 0, pageSize: 10 })
const table = useReactTable({
data, columns,
getCoreRowModel: getCoreRowModel(),
manualPagination: true,
rowCount: serverData.totalRows,
state: { pagination },
onPaginationChange: setPagination,
})
// Row selection with checkbox column
columnHelper.display({
id: 'select',
header: ({ table }) => (
<input type="checkbox" checked={table.getIsAllRowsSelected()}
/>
),
cell: ({ row }) => (
<input type="checkbox" checked={row.getIsSelected()}
disabled={!row.getCanSelect()}
/>
),
})
// Column filtering
const [columnFilters, setColumnFilters] = useState<ColumnFiltersState>([])
const table = useReactTable({
data, columns,
getCoreRowModel: getCoreRowModel(),
getFilteredRowModel: getFilteredRowModel(),
state: { columnFilters },
onColumnFiltersChange: setColumnFilters,
})
// Filter input
<input value={column.getFilterValue() ?? ''} => column.setFilterValue(e.target.value)} />
// Stable row IDs for selection across re-fetches
const table = useReactTable({
data, columns,
getRowId: (row) => row.uuid,
getCoreRowModel: getCoreRowModel(),
})
1---2name: tanstack-table3description: TanStack Table best practices for building headless, type-safe data tables in React with sorting, filtering, pagination, row selection, and column management. Use when building data grids, implementing client-side or server-side table features, defining column structures, managing table state, or optimizing table rendering performance.4---56# TanStack Table78**Version**: @tanstack/react-table@latest9**Requires**: React 16.8+, TypeScript recommended1011## Quick Setup1213```bash14npm install @tanstack/react-table15```1617```tsx18import {19 useReactTable,20 getCoreRowModel,21 flexRender,22 createColumnHelper,23} from '@tanstack/react-table'2425type User = {26 name: string27 age: number28 status: string29}3031const columnHelper = createColumnHelper<User>()3233const columns = [34 columnHelper.accessor('name', { header: 'Name' }),35 columnHelper.accessor('age', { header: 'Age' }),36 columnHelper.accessor('status', { header: 'Status' }),37 columnHelper.display({38 id: 'actions',39 cell: (props) => <button onClick={() => edit(props.row.original)}>Edit</button>,40 }),41]4243function App() {44 const [data] = useState<User[]>([]) // must be stable reference4546 const table = useReactTable({47 data,48 columns,49 getCoreRowModel: getCoreRowModel(),50 })5152 return (53 <table>54 <thead>55 {table.getHeaderGroups().map((headerGroup) => (56 <tr key={headerGroup.id}>57 {headerGroup.headers.map((header) => (58 <th key={header.id}>59 {flexRender(header.column.columnDef.header, header.getContext())}60 </th>61 ))}62 </tr>63 ))}64 </thead>65 <tbody>66 {table.getRowModel().rows.map((row) => (67 <tr key={row.id}>68 {row.getVisibleCells().map((cell) => (69 <td key={cell.id}>70 {flexRender(cell.column.columnDef.cell, cell.getContext())}71 </td>72 ))}73 </tr>74 ))}75 </tbody>76 </table>77 )78}79```8081### Row Models (Import Only What You Need)8283TanStack Table is modular. Only import the row models you actually use:8485```tsx86import {87 getCoreRowModel, // required88 getSortedRowModel, // client-side sorting89 getFilteredRowModel, // client-side filtering90 getPaginationRowModel,// client-side pagination91 getExpandedRowModel, // expanding/sub-rows92 getGroupedRowModel, // grouping + aggregation93 getFacetedRowModel, // faceted values94 getFacetedUniqueValues,95 getFacetedMinMaxValues,96} from '@tanstack/react-table'97```9899Pipeline order: Core -> Filtered -> Grouped -> Sorted -> Expanded -> Paginated -> Rendered100101## Rule Categories102103| Priority | Category | Rule File | Impact |104|----------|----------|-----------|--------|105| CRITICAL | Table Setup | `rules/table-setup.md` | Correct table creation, stable data references |106| CRITICAL | Column Definitions | `rules/col-column-defs.md` | Data model, rendering, type safety |107| CRITICAL | Row Models | `rules/rm-row-models.md` | Modular imports, pipeline order |108| HIGH | Sorting | `rules/sort-sorting.md` | Client/server sorting, custom sort functions |109| HIGH | Column Filtering | `rules/filt-column-filtering.md` | Per-column filters, custom filter functions |110| HIGH | Global Filtering | `rules/filt-global-filtering.md` | Table-wide search, global filter function |111| HIGH | Pagination | `rules/pag-pagination.md` | Client/server pagination, page state |112| MEDIUM | Row Selection | `rules/sel-row-selection.md` | Checkbox/radio selection, selection state |113| MEDIUM | Column Visibility | `rules/vis-column-visibility.md` | Show/hide columns dynamically |114| MEDIUM | Column Sizing | `rules/size-column-sizing.md` | Widths, resizing, performance |115| LOW | Expanding | `rules/exp-expanding.md` | Sub-rows, detail panels, hierarchical data |116117## Critical Rules118119### Always Do120121- **Stable `data` reference** — use `useState`, `useMemo`, or define outside component to prevent infinite re-renders122- **Use `createColumnHelper<TData>()`** — for maximum type safety in column definitions123- **Import only needed row models** — don't import `getSortedRowModel` if you don't sort client-side124- **Use `flexRender`** — for rendering header/cell/footer templates from column defs125- **Use `getVisibleCells()`** — not `getAllCells()`, to respect column visibility126- **Use `getRowModel()`** — the final row model that applies all features (filtering, sorting, pagination)127- **Control state with `state` + `on*Change`** — for sorting, filtering, pagination, selection, etc.128129### Never Do130131- **Define `data` inline** — `useReactTable({ data: fetchData() })` causes infinite re-renders132- **Define `columns` inside render** — columns array must be stable (define outside component or `useMemo`)133- **Use `getAllCells()` for rendering** — ignores column visibility; use `getVisibleCells()`134- **Mix `initialState` and `state` for the same feature** — `state` overrides `initialState`135- **Use client-side row models with `manual*` options** — if `manualSorting: true`, don't import `getSortedRowModel`136- **Forget `getRowId`** — without it, row IDs default to index, breaking selection state across re-fetches137138## Key Patterns139140```tsx141// Controlled sorting state142const [sorting, setSorting] = useState<SortingState>([])143const table = useReactTable({144 data, columns,145 getCoreRowModel: getCoreRowModel(),146 getSortedRowModel: getSortedRowModel(),147 state: { sorting },148 onSortingChange: setSorting,149})150// Header click handler151<th onClick={header.column.getToggleSortingHandler()}>152 {flexRender(header.column.columnDef.header, header.getContext())}153 {{ asc: ' 🔼', desc: ' 🔽' }[header.column.getIsSorted() as string] ?? ''}154</th>155156// Server-side pagination157const [pagination, setPagination] = useState({ pageIndex: 0, pageSize: 10 })158const table = useReactTable({159 data, columns,160 getCoreRowModel: getCoreRowModel(),161 manualPagination: true,162 rowCount: serverData.totalRows,163 state: { pagination },164 onPaginationChange: setPagination,165})166167// Row selection with checkbox column168columnHelper.display({169 id: 'select',170 header: ({ table }) => (171 <input type="checkbox" checked={table.getIsAllRowsSelected()}172 onChange={table.getToggleAllRowsSelectedHandler()} />173 ),174 cell: ({ row }) => (175 <input type="checkbox" checked={row.getIsSelected()}176 disabled={!row.getCanSelect()}177 onChange={row.getToggleSelectedHandler()} />178 ),179})180181// Column filtering182const [columnFilters, setColumnFilters] = useState<ColumnFiltersState>([])183const table = useReactTable({184 data, columns,185 getCoreRowModel: getCoreRowModel(),186 getFilteredRowModel: getFilteredRowModel(),187 state: { columnFilters },188 onColumnFiltersChange: setColumnFilters,189})190// Filter input191<input value={column.getFilterValue() ?? ''} onChange={e => column.setFilterValue(e.target.value)} />192193// Stable row IDs for selection across re-fetches194const table = useReactTable({195 data, columns,196 getRowId: (row) => row.uuid,197 getCoreRowModel: getCoreRowModel(),198})199```