tanstack-table
TanStack Table v8 Best Practices
Core Concepts
TanStack Table is headless — it provides table logic and state, not UI. You bring your own markup and styling. This means full control over rendering with zero style conflicts.
Basic Setup
import { useReactTable, getCoreRowModel, flexRender } from "@tanstack/react-table";
function UsersTable({ data }: { data: User[] }) {
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>
);
}
Column Definitions
Define columns outside the component or memoize them to prevent infinite re-renders:
const columnHelper = createColumnHelper<User>();
const columns = [
columnHelper.accessor("name", {
header: "Name",
cell: (info) => info.getValue(),
}),
columnHelper.accessor("email", {
header: "Email",
}),
columnHelper.accessor("role", {
header: "Role",
cell: (info) => <Badge>{info.getValue()}</Badge>,
filterFn: "equalsString",
}),
columnHelper.accessor("createdAt", {
header: "Created",
cell: (info) => formatDate(info.getValue()),
sortingFn: "datetime",
}),
columnHelper.display({
id: "actions",
header: "Actions",
cell: ({ row }) => <RowActions user={row.original} />,
}),
];
- Use
columnHelperfor type-safe column definitions. accessorcolumns map to data fields with automatic type inference.displaycolumns are for UI-only elements (actions, checkboxes, expand toggles).
Data Stability
Data must have a stable reference. Passing a new array on every render causes infinite re-renders:
// Stable via useState
const [data, setData] = useState<User[]>([]);
// Stable via useMemo
const data = useMemo(() => transformRawData(rawData), [rawData]);
// Stable via TanStack Query (recommended)
const { data = [] } = useUsers();
Sorting
const table = useReactTable({
data,
columns,
getCoreRowModel: getCoreRowModel(),
getSortedRowModel: getSortedRowModel(),
state: { sorting },
onSortingChange: setSorting,
});
Toggle sorting on headers:
<th
onClick={header.column.getToggleSortingHandler()}
style={{ cursor: header.column.getCanSort() ? "pointer" : "default" }}
>
{flexRender(header.column.columnDef.header, header.getContext())}
{{ asc: " ↑", desc: " ↓" }[header.column.getIsSorted() as string] ?? null}
</th>
Filtering
Column Filters
const table = useReactTable({
data,
columns,
getCoreRowModel: getCoreRowModel(),
getFilteredRowModel: getFilteredRowModel(),
state: { columnFilters },
onColumnFiltersChange: setColumnFilters,
});
Global Filter
const table = useReactTable({
// ...
state: { globalFilter },
onGlobalFilterChange: setGlobalFilter,
globalFilterFn: "includesString",
});
Custom Filter Functions
const columns = [
columnHelper.accessor("price", {
filterFn: (row, columnId, filterValue) => {
const price = row.getValue<number>(columnId);
const { min, max } = filterValue as { min: number; max: number };
return price >= min && price <= max;
},
}),
];
Pagination
const table = useReactTable({
data,
columns,
getCoreRowModel: getCoreRowModel(),
getPaginationRowModel: getPaginationRowModel(),
state: { pagination },
onPaginationChange: setPagination,
});
// Controls
table.getCanPreviousPage();
table.getCanNextPage();
table.previousPage();
table.nextPage();
table.setPageIndex(0);
table.setPageSize(20);
table.getPageCount();
Server-Side Operations
For large datasets, handle sorting/filtering/pagination on the server:
const table = useReactTable({
data: serverData,
columns,
getCoreRowModel: getCoreRowModel(),
manualPagination: true,
manualSorting: true,
manualFiltering: true,
pageCount: totalPages,
state: { pagination, sorting, columnFilters },
onPaginationChange: setPagination,
onSortingChange: setSorting,
onColumnFiltersChange: setColumnFilters,
});
Pass the state to your API query (pairs well with TanStack Query):
const { data } = useQuery({
queryKey: ["users", pagination, sorting, columnFilters],
queryFn: () => fetchUsers({ pagination, sorting, filters: columnFilters }),
});
Row Selection
const table = useReactTable({
data,
columns,
getCoreRowModel: getCoreRowModel(),
state: { rowSelection },
onRowSelectionChange: setRowSelection,
enableRowSelection: true,
// enableMultiRowSelection: false, // single-select mode
});
// Checkbox column
columnHelper.display({
id: "select",
header: ({ table }) => (
<Checkbox
checked={table.getIsAllRowsSelected()}
indeterminate={table.getIsSomeRowsSelected()}
onChange={table.getToggleAllRowsSelectedHandler()}
/>
),
cell: ({ row }) => <Checkbox checked={row.getIsSelected()} onChange={row.getToggleSelectedHandler()} />,
});
Column Visibility
const table = useReactTable({
data,
columns,
getCoreRowModel: getCoreRowModel(),
state: { columnVisibility },
onColumnVisibilityChange: setColumnVisibility,
});
// Toggle UI
table.getAllLeafColumns().map((column) => (
<label key={column.id}>
<input type="checkbox" checked={column.getIsVisible()} onChange={column.getToggleVisibilityHandler()} />
{column.id}
</label>
));
Virtualization
For tables with thousands of rows, combine with @tanstack/react-virtual:
import { useVirtualizer } from "@tanstack/react-virtual";
const rowVirtualizer = useVirtualizer({
count: table.getRowModel().rows.length,
getScrollElement: () => tableContainerRef.current,
estimateSize: () => 48,
overscan: 10,
});
Render only the visible rows within a scrollable container.
Patterns
- Wrap in a custom hook — encapsulate table state and configuration in a
useUsersTable(data)hook. - Extract filter/pagination UI — build reusable
TablePagination,TableFilter, andColumnTogglecomponents that accept the table instance. - Type your row data — always define
TDataand pass it as a generic tocreateColumnHelper<TData>(). - Stable column definitions — define columns at module scope or memoize with
useMemo. Never define them inline in the render body.
More from grahamcrackers/skills
bulletproof-react-patterns
Bulletproof React architecture patterns for scalable, maintainable applications. Covers feature-based project structure, component patterns, state management boundaries, API layer design, error handling, security, and testing strategies. Use when structuring a React project, designing application architecture, organizing features, or when the user asks about React project structure or scalable patterns.
45react-aria-components
React Aria Components patterns for building accessible, unstyled UI with composition-based architecture. Covers component structure, styling with Tailwind and CSS, render props, collections, forms, selections, overlays, and drag-and-drop. Use when building accessible components, using react-aria-components, creating design systems, or when the user asks about React Aria, accessible UI primitives, or headless component libraries.
17clean-code-principles
Clean code principles for readable, maintainable TypeScript and React codebases. Covers naming, functions, abstraction, composition, error handling, comments, and code smells. Use when writing new code, refactoring, reviewing code quality, or when the user asks about clean code, readability, or maintainability.
10typescript-best-practices
Core TypeScript conventions for type safety, inference, and clean code. Use when writing TypeScript, reviewing TypeScript code, creating interfaces/types, or when the user asks about TypeScript patterns, conventions, or best practices.
9tanstack-query
TanStack Query v5 patterns for server state management, caching, mutations, optimistic updates, and query organization. Use when working with TanStack Query, React Query, server state, data fetching hooks, or when the user asks about caching strategies, query invalidation, or mutation patterns.
8zustand
Zustand state management patterns for React including store design, selectors, slices, middleware (immer, persist, devtools), and async actions. Use when managing client-side state, creating stores, working with Zustand, or when the user asks about global state management, store patterns, or state persistence.
7