Tanstack table
Skill tanstack-skills/tanstack-skills/plugins/tanstack-table/skills/tanstack-table
UNOFFICIAL Claude Code TanStack Skills
npx -y skills add tanstack-skills/tanstack-skills --skill tanstack-tableAssembled from the repository path, not quoted from the project. Check it against their README if it does not work.
What its author says it does
Copied from the file, not written here
Headless UI for building powerful tables & datagrids for TS/JS, React, Vue, Solid, Svelte, Qwik, Angular, and Lit.
SKILL.md
15.1 KB, as published. Nobody here has run it
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
// WRONG - new references every render, causes infinite loops
const table = useReactTable({
data: fetchedData.results, // new ref!
columns: [{ accessorKey: 'name' }], // new ref!
})
// CORRECT - stable references
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 = [
// Accessor column (data column)
columnHelper.accessor('firstName', {
header: 'First Name',
cell: info => info.getValue(),
footer: info => info.column.id,
}),
// Accessor with function
columnHelper.accessor(row => row.lastName, {
id: 'lastName', // required with accessorFn
header: () => <span>Last Name</span>,
cell: info => <i>{info.getValue()}</i>,
}),
// Display column (no data, custom rendering)
columnHelper.display({
id: 'actions',
header: 'Actions',
cell: ({ row }) => (
<button onClick={() => deleteRow(row.original)}>Delete</button>
),
}),
// Group column (nested headers)
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,
// manualSorting: true, // For server-side sorting
})
// Built-in sort functions: 'alphanumeric', 'text', 'datetime', 'basic'
// Column-level: sortingFn: 'alphanumeric'
Filtering
Column Filtering
const table = useReactTable({
state: { columnFilters },
onColumnFiltersChange: setColumnFilters,
getFilteredRowModel: getFilteredRowModel(),
getFacetedRowModel: getFacetedRowModel(),
getFacetedUniqueValues: getFacetedUniqueValues(),
getFacetedMinMaxValues: getFacetedMinMaxValues(),
})
// Built-in: 'includesString', 'equalsString', 'arrIncludes', 'inNumberRange', etc.
// Filter UI
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(),
// For server-side:
// manualPagination: true,
// pageCount: serverPageCount,
})
// Navigation
table.nextPage()
table.previousPage()
table.firstPage()
table.lastPage()
table.setPageSize(20)
table.getCanNextPage() // boolean
table.getCanPreviousPage() // boolean
table.getPageCount() // total pages
Row Selection
const [rowSelection, setRowSelection] = useState<RowSelectionState>({})
const table = useReactTable({
state: { rowSelection },
onRowSelectionChange: setRowSelection,
enableRowSelection: true,
enableMultiRowSelection: true,
})
// Checkbox column
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()}
/>
),
})
// Get selected rows
table.getSelectedRowModel().rows
Column Visibility
const [columnVisibility, setColumnVisibility] = useState<VisibilityState>({})
const table = useReactTable({
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>
))}
Column Pinning
const [columnPinning, setColumnPinning] = useState<ColumnPinningState>({
left: ['select', 'name'],
right: ['actions'],
})
const table = useReactTable({
state: { columnPinning },
onColumnPinningChange: setColumnPinning,
enableColumnPinning: true,
})
// Render pinned sections separately
row.getLeftVisibleCells() // Left-pinned
row.getCenterVisibleCells() // Unpinned
row.getRightVisibleCells() // Right-pinned
Column Resizing
const table = useReactTable({
enableColumnResizing: true,
columnResizeMode: 'onChange', // 'onChange' | 'onEnd'
defaultColumn: { size: 150, minSize: 50, maxSize: 500 },
})
// Resize handle in header
<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(),
})
// Built-in aggregation: 'sum', 'min', 'max', 'mean', 'median', 'count', 'unique', 'uniqueCount'
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, // For hierarchical data
})
// Expand toggle
<button onClick={row.getToggleExpandedHandler()}>
{row.getIsExpanded() ? '−' : '+'}
</button>
// Detail row pattern
{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(),
// Do NOT include getSortedRowModel, getFilteredRowModel, getPaginationRowModel
})
// Fetch data based on state
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
dataandcolumnsto prevent infinite re-renders - Use
flexRenderfor all header/cell/footer rendering - Use
table.getRowModel().rowsfor final rendered rows (not getCoreRowModel) - Import only needed row models - each adds processing to the pipeline
- Use
getRowIdfor stable row keys when data has unique IDs - Use
manualXoptions for server-side operations - Pair controlled state with both
state.XandonXChange - Use module augmentation for custom meta, filter fns, sort fns
- Use column helper for type-safe column definitions
- Set
autoResetPageIndex: truewhen 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
idwhen usingaccessorFn - Mixing
manualPaginationwith client-sidegetPaginationRowModel - Forgetting
colSpanfor grouped headers - Not handling
header.isPlaceholderfor group column spacers