tanstack-table

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.

Safety Notice

This listing is imported from skills.sh public index metadata. Review upstream SKILL.md and repository scripts before running.

Copy this and send it to your AI assistant to learn

Install skill "tanstack-table" with this command: npx skills add tanstack-skills/tanstack-skills/tanstack-skills-tanstack-skills-tanstack-table

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 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

Source Transparency

This detail page is rendered from real SKILL.md content. Trust labels are metadata-based hints, not a safety guarantee.

Related Skills

Related by shared tags or category signals.

General

tanstack table

No summary provided by upstream source.

Repository SourceNeeds Review
-251
jezweb
General

tanstack-query

No summary provided by upstream source.

Repository SourceNeeds Review
General

tanstack-router

No summary provided by upstream source.

Repository SourceNeeds Review