| name | tanstack-table |
| description | Headless UI for building powerful tables & datagrids for TS/JS, React, Vue, Solid, Svelte, Qwik, Angular, and Lit. |
Overview
TanStack Table is a headless UI library for building data tables and datagrids. It provides logic for sorting, filtering, pagination, grouping, expanding, column pinning/ordering/visibility/resizing, and row selection - without rendering any markup or styles.
Package: @tanstack/react-table
Utilities: @tanstack/match-sorter-utils (fuzzy filtering)
Current Version: v8
Installation
npm install @tanstack/react-table
Core Architecture
Building Blocks
- Column Definitions - describe columns (data access, rendering, features)
- Table Instance - central coordinator with state and APIs
- Row Models - data processing pipeline (filter -> sort -> group -> paginate)
- Headers, Rows, Cells - renderable units
Critical: Data & Column Stability
const table = useReactTable({
data: fetchedData.results,
columns: [{ accessorKey: 'name' }],
})
const columns = useMemo(() => [...], [])
const data = useMemo(() => fetchedData?.results ?? [], [fetchedData])
const table = useReactTable({ data, columns, getCoreRowModel: getCoreRowModel() })
Column Definitions
Using createColumnHelper (Recommended)
import { createColumnHelper } from '@tanstack/react-table'
type Person = {
firstName: string
lastName: string
age: number
status: 'active' | 'inactive'
}
const columnHelper = createColumnHelper<Person>()
const columns = [
columnHelper.accessor('firstName', {
header: 'First Name',
cell: info => info.getValue(),
footer: info => info.column.id,
}),
columnHelper.accessor(row => row.lastName, {
id: 'lastName',
header: () => <span>Last Name</span>,
cell: info => <i>{info.getValue()}</i>,
}),
columnHelper.display({
id: 'actions',
header: 'Actions',
cell: ({ row }) => (
<button onClick={() => deleteRow(row.original)}>Delete</button>
),
}),
columnHelper.group({
id: 'info',
header: 'Info',
columns: [
columnHelper.accessor('age', { header: 'Age' }),
columnHelper.accessor('status', { header: 'Status' }),
],
}),
]
Column Options
| Option | Type | Description |
|---|
id | string | Unique identifier (auto-derived from accessorKey) |
accessorKey | string | Dot-notation path to row data |
accessorFn | (row) => any | Custom accessor function |
header | string | (context) => ReactNode | Header renderer |
cell | (context) => ReactNode | Cell renderer |
footer | (context) => ReactNode | Footer renderer |
size | number | Default width (default: 150) |
minSize | number | Min width (default: 20) |
maxSize | number | Max width |
enableSorting | boolean | Enable sorting |
sortingFn | string | SortingFn | Sort function |
enableFiltering | boolean | Enable filtering |
filterFn | string | FilterFn | Filter function |
enableGrouping | boolean | Enable grouping |
aggregationFn | string | AggregationFn | Aggregation function |
enableHiding | boolean | Enable visibility toggle |
enableResizing | boolean | Enable resizing |
enablePinning | boolean | Enable pinning |
meta | any | Custom metadata |
Table Instance
Creating a Table
import {
useReactTable,
getCoreRowModel,
getSortedRowModel,
getFilteredRowModel,
getPaginationRowModel,
flexRender,
} from '@tanstack/react-table'
function MyTable() {
const [sorting, setSorting] = useState<SortingState>([])
const [columnFilters, setColumnFilters] = useState<ColumnFiltersState>([])
const [pagination, setPagination] = useState<PaginationState>({
pageIndex: 0,
pageSize: 10,
})
const table = useReactTable({
data,
columns,
state: { sorting, columnFilters, pagination },
onSortingChange: setSorting,
onColumnFiltersChange: setColumnFilters,
onPaginationChange: setPagination,
getCoreRowModel: getCoreRowModel(),
getSortedRowModel: getSortedRowModel(),
getFilteredRowModel: getFilteredRowModel(),
getPaginationRowModel: getPaginationRowModel(),
})
return (
<table>
<thead>
{table.getHeaderGroups().map(headerGroup => (
<tr key={headerGroup.id}>
{headerGroup.headers.map(header => (
<th key={header.id} onClick={header.column.getToggleSortingHandler()}>
{header.isPlaceholder ? null :
flexRender(header.column.columnDef.header, header.getContext())}
{{ asc: ' ↑', desc: ' ↓' }[header.column.getIsSorted() as string] ?? null}
</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>
)
}
Sorting
const table = useReactTable({
state: { sorting },
onSortingChange: setSorting,
getSortedRowModel: getSortedRowModel(),
enableSorting: true,
enableMultiSort: true,
});
Filtering
Column Filtering
const table = useReactTable({
state: { columnFilters },
onColumnFiltersChange: setColumnFilters,
getFilteredRowModel: getFilteredRowModel(),
getFacetedRowModel: getFacetedRowModel(),
getFacetedUniqueValues: getFacetedUniqueValues(),
getFacetedMinMaxValues: getFacetedMinMaxValues(),
})
function Filter({ column }) {
return (
<input
value={(column.getFilterValue() ?? '') as string}
onChange={e => column.setFilterValue(e.target.value)}
placeholder={`Filter... (${column.getFacetedUniqueValues()?.size})`}
/>
)
}
Global Filtering
const [globalFilter, setGlobalFilter] = useState("");
const table = useReactTable({
state: { globalFilter },
onGlobalFilterChange: setGlobalFilter,
globalFilterFn: "includesString",
getFilteredRowModel: getFilteredRowModel(),
});
Fuzzy Filtering
import { rankItem } from "@tanstack/match-sorter-utils";
const fuzzyFilter: FilterFn<any> = (row, columnId, value, addMeta) => {
const itemRank = rankItem(row.getValue(columnId), value);
addMeta({ itemRank });
return itemRank.passed;
};
const table = useReactTable({
filterFns: { fuzzy: fuzzyFilter },
globalFilterFn: "fuzzy",
});
Pagination
const table = useReactTable({
state: { pagination },
onPaginationChange: setPagination,
getPaginationRowModel: getPaginationRowModel(),
});
table.nextPage();
table.previousPage();
table.firstPage();
table.lastPage();
table.setPageSize(20);
table.getCanNextPage();
table.getCanPreviousPage();
table.getPageCount();
Row Selection
const [rowSelection, setRowSelection] = useState<RowSelectionState>({})
const table = useReactTable({
state: { rowSelection },
onRowSelectionChange: setRowSelection,
enableRowSelection: true,
enableMultiRowSelection: true,
})
columnHelper.display({
id: 'select',
header: ({ table }) => (
<input
type="checkbox"
checked={table.getIsAllRowsSelected()}
onChange={table.getToggleAllRowsSelectedHandler()}
/>
),
cell: ({ row }) => (
<input
type="checkbox"
checked={row.getIsSelected()}
disabled={!row.getCanSelect()}
onChange={row.getToggleSelectedHandler()}
/>
),
})
table.getSelectedRowModel().rows
Column Visibility
const [columnVisibility, setColumnVisibility] = useState<VisibilityState>({})
const table = useReactTable({
state: { columnVisibility },
onColumnVisibilityChange: setColumnVisibility,
})
{table.getAllLeafColumns().map(column => (
<label key={column.id}>
<input
type="checkbox"
checked={column.getIsVisible()}
onChange={column.getToggleVisibilityHandler()}
/>
{column.id}
</label>
))}
Column Pinning
const [columnPinning, setColumnPinning] = useState<ColumnPinningState>({
left: ["select", "name"],
right: ["actions"],
});
const table = useReactTable({
state: { columnPinning },
onColumnPinningChange: setColumnPinning,
enableColumnPinning: true,
});
row.getLeftVisibleCells();
row.getCenterVisibleCells();
row.getRightVisibleCells();
Column Resizing
const table = useReactTable({
enableColumnResizing: true,
columnResizeMode: 'onChange',
defaultColumn: { size: 150, minSize: 50, maxSize: 500 },
})
<div
onMouseDown={header.getResizeHandler()}
onTouchStart={header.getResizeHandler()}
className={`resizer ${header.column.getIsResizing() ? 'isResizing' : ''}`}
/>
Grouping & Aggregation
const [grouping, setGrouping] = useState<GroupingState>([]);
const table = useReactTable({
state: { grouping },
onGroupingChange: setGrouping,
getGroupedRowModel: getGroupedRowModel(),
getExpandedRowModel: getExpandedRowModel(),
});
columnHelper.accessor("amount", {
aggregationFn: "sum",
aggregatedCell: ({ getValue }) => `Total: ${getValue()}`,
});
Row Expanding
const [expanded, setExpanded] = useState<ExpandedState>({})
const table = useReactTable({
state: { expanded },
onExpandedChange: setExpanded,
getExpandedRowModel: getExpandedRowModel(),
getSubRows: (row) => row.subRows,
})
<button onClick={row.getToggleExpandedHandler()}>
{row.getIsExpanded() ? '−' : '+'}
</button>
{row.getIsExpanded() && (
<tr>
<td colSpan={columns.length}>
<DetailComponent data={row.original} />
</td>
</tr>
)}
Virtualization Integration
import { useVirtualizer } from '@tanstack/react-virtual'
function VirtualizedTable() {
const table = useReactTable({ })
const { rows } = table.getRowModel()
const parentRef = useRef<HTMLDivElement>(null)
const virtualizer = useVirtualizer({
count: rows.length,
getScrollElement: () => parentRef.current,
estimateSize: () => 35,
overscan: 10,
})
return (
<div ref={parentRef} style={{ height: '600px', overflow: 'auto' }}>
<table>
<tbody style={{ height: `${virtualizer.getTotalSize()}px`, position: 'relative' }}>
{virtualizer.getVirtualItems().map(virtualRow => {
const row = rows[virtualRow.index]
return (
<tr
key={row.id}
style={{
position: 'absolute',
transform: `translateY(${virtualRow.start}px)`,
height: `${virtualRow.size}px`,
}}
>
{row.getVisibleCells().map(cell => (
<td key={cell.id}>
{flexRender(cell.column.columnDef.cell, cell.getContext())}
</td>
))}
</tr>
)
})}
</tbody>
</table>
</div>
)
}
Server-Side Operations
const table = useReactTable({
data: serverData,
columns,
manualSorting: true,
manualFiltering: true,
manualPagination: true,
pageCount: serverPageCount,
state: { sorting, columnFilters, pagination },
onSortingChange: setSorting,
onColumnFiltersChange: setColumnFilters,
onPaginationChange: setPagination,
getCoreRowModel: getCoreRowModel(),
});
useEffect(() => {
fetchData({ sorting, filters: columnFilters, pagination });
}, [sorting, columnFilters, pagination]);
TypeScript Patterns
Extending Column Meta
declare module "@tanstack/react-table" {
interface ColumnMeta<TData extends RowData, TValue> {
filterVariant?: "text" | "range" | "select";
align?: "left" | "center" | "right";
}
}
Custom Filter/Sort Function Registration
declare module "@tanstack/react-table" {
interface FilterFns {
fuzzy: FilterFn<unknown>;
}
interface SortingFns {
myCustomSort: SortingFn<unknown>;
}
}
Editable Cells via Table Meta
declare module "@tanstack/react-table" {
interface TableMeta<TData extends RowData> {
updateData: (rowIndex: number, columnId: string, value: unknown) => void;
}
}
const table = useReactTable({
meta: {
updateData: (rowIndex, columnId, value) => {
setData((old) => old.map((row, i) => (i === rowIndex ? { ...row, [columnId]: value } : row)));
},
},
});
Key Imports
import {
createColumnHelper,
flexRender,
useReactTable,
getCoreRowModel,
getSortedRowModel,
getFilteredRowModel,
getPaginationRowModel,
getGroupedRowModel,
getExpandedRowModel,
getFacetedRowModel,
getFacetedUniqueValues,
getFacetedMinMaxValues,
} from "@tanstack/react-table";
import type {
ColumnDef,
SortingState,
ColumnFiltersState,
VisibilityState,
PaginationState,
ExpandedState,
RowSelectionState,
GroupingState,
ColumnOrderState,
ColumnPinningState,
FilterFn,
SortingFn,
} from "@tanstack/react-table";
Best Practices
- Always memoize
data and columns to prevent infinite re-renders
- Use
flexRender for all header/cell/footer rendering
- Use
table.getRowModel().rows for final rendered rows (not getCoreRowModel)
- Import only needed row models - each adds processing to the pipeline
- Use
getRowId for stable row keys when data has unique IDs
- Use
manualX options for server-side operations
- Pair controlled state with both
state.X and onXChange
- Use module augmentation for custom meta, filter fns, sort fns
- Use column helper for type-safe column definitions
- Set
autoResetPageIndex: true when filtering should reset pagination
Common Pitfalls
- Defining columns inline (creates new ref each render)
- Forgetting
getCoreRowModel() (required for all tables)
- Using row models without importing them
- Not providing
id when using accessorFn
- Mixing
manualPagination with client-side getPaginationRowModel
- Forgetting
colSpan for grouped headers
- Not handling
header.isPlaceholder for group column spacers