grahamcrackers/skills

tanstack-table

TanStack Table v8 patterns for building headless, type-safe tables in React with sorting, filtering, pagination, and virtualization.

First seen Feb 28, 2026

Installation

$ npx skills add grahamcrackers/skills --skill tanstack-table

Summary

  • TanStack Table v8 patterns for building headless, type-safe tables in React with sorting, filtering, pagination, and virtualization.
  • Use when building data tables, grids, or list views, or when the user asks about TanStack Table, React Table, column definitions, or table state management.

Similar popular skills

Related neighbors and high-traction skills in the same topics — useful to compare before installing.

Also in this package

Other skills from grahamcrackers/skills · top by installs.

npx skills add grahamcrackers/skills

Browse all from grahamcrackers/skills

More details

Agent compatibility

Declared targets from SKILL.md / docs. Unmarked agents are not listed — the skill may still install via the CLI.

Claude Code Not declared
Cursor Not declared
Codex Not declared
GitHub Copilot Not declared
Windsurf Not declared
Gemini CLI Not declared
Cline Not declared
OpenCode Not declared

Repository health

License LICENSE
Default branch main
Open issues 0
Status Active

Package contents

Files included with this skill beyond the listing page.

  • skill md SKILL.md 7,732 B
  • docs SUMMARY.md 311 B

History

  1. First seen on skills.sh
  2. First recorded snapshot · 7 installs

SKILL.md

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 columnHelper for type-safe column definitions.
  • accessor columns map to data fields with automatic type inference.
  • display columns 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, and ColumnToggle components that accept the table instance.
  • Type your row data — always define TData and pass it as a generic to createColumnHelper<TData>().
  • Stable column definitions — define columns at module scope or memoize with useMemo. Never define them inline in the render body.