Tanstack Table

作者 tanstack-skills6f5521ecbdfb无许可证35 个星标收录于 2026年10月8日更新于 2026年10月8日仓库8个月前更新

Headless UI for building powerful tables & datagrids for TS/JS, React, Vue, Solid, Svelte, Qwik, Angular, and Lit.

AI 生成的概览

使用 TanStack Table 库在 TypeScript 和 JavaScript 中构建无头数据表格与数据网格的参考指南。

功能
该技能说明如何使用 TanStack Table 构建无头数据表格与数据网格,涵盖列定义、表格实例和行模型。它讲解排序、筛选、分页、分组、聚合、行选择、列可见性、固定、调整大小和展开等功能,并配有代码示例。内容还涉及虚拟化集成、服务端操作、TypeScript 模式、最佳实践和常见陷阱。
适用场景
在 TypeScript 或 JavaScript 前端(尤其是 React)中实现数据表格或数据网格时使用。适合涉及表格界面中排序、筛选、分页、分组或行选择的任务。也适用于接入服务端表格操作或虚拟化行。
运行要求
需要 @tanstack/react-table 包(v8),模糊筛选可选 @tanstack/match-sorter-utils,虚拟化可选 @tanstack/react-virtual。该技能不附带脚本,仅为说明和代码示例。

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

bash
npm install @tanstack/react-table

Core Architecture

Building Blocks

  1. Column Definitions - describe columns (data access, rendering, features)
  2. Table Instance - central coordinator with state and APIs
  3. Row Models - data processing pipeline (filter -> sort -> group -> paginate)
  4. Headers, Rows, Cells - renderable units

Critical: Data & Column Stability

typescript
// WRONG - new references every render, causes infinite loopsconst table = useReactTable({  data: fetchedData.results,     // new ref!  columns: [{ accessorKey: 'name' }], // new ref!})
// CORRECT - stable referencesconst columns = useMemo(() => [...], [])const data = useMemo(() => fetchedData?.results ?? [], [fetchedData])
const table = useReactTable({ data, columns, getCoreRowModel: getCoreRowModel() })

Column Definitions

Using createColumnHelper (Recommended)

typescript
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

OptionTypeDescription
idstringUnique identifier (auto-derived from accessorKey)
accessorKeystringDot-notation path to row data
accessorFn(row) => anyCustom accessor function
headerstring | (context) => ReactNodeHeader renderer
cell(context) => ReactNodeCell renderer
footer(context) => ReactNodeFooter renderer
sizenumberDefault width (default: 150)
minSizenumberMin width (default: 20)
maxSizenumberMax width
enableSortingbooleanEnable sorting
sortingFnstring | SortingFnSort function
enableFilteringbooleanEnable filtering
filterFnstring | FilterFnFilter function
enableGroupingbooleanEnable grouping
aggregationFnstring | AggregationFnAggregation function
enableHidingbooleanEnable visibility toggle
enableResizingbooleanEnable resizing
enablePinningbooleanEnable pinning
metaanyCustom metadata

Table Instance

Creating a Table

typescript
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

typescript
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

typescript
const table = useReactTable({  state: { columnFilters },  onColumnFiltersChange: setColumnFilters,  getFilteredRowModel: getFilteredRowModel(),  getFacetedRowModel: getFacetedRowModel(),  getFacetedUniqueValues: getFacetedUniqueValues(),  getFacetedMinMaxValues: getFacetedMinMaxValues(),})
// Built-in: 'includesString', 'equalsString', 'arrIncludes', 'inNumberRange', etc.
// Filter UIfunction Filter({ column }) {  return (    <input      value={(column.getFilterValue() ?? '') as string}      onChange={e => column.setFilterValue(e.target.value)}      placeholder={`Filter... (${column.getFacetedUniqueValues()?.size})`}    />  )}

Global Filtering

typescript
const [globalFilter, setGlobalFilter] = useState('')
const table = useReactTable({  state: { globalFilter },  onGlobalFilterChange: setGlobalFilter,  globalFilterFn: 'includesString',  getFilteredRowModel: getFilteredRowModel(),})

Fuzzy Filtering

typescript
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

typescript
const table = useReactTable({  state: { pagination },  onPaginationChange: setPagination,  getPaginationRowModel: getPaginationRowModel(),  // For server-side:  // manualPagination: true,  // pageCount: serverPageCount,})
// Navigationtable.nextPage()table.previousPage()table.firstPage()table.lastPage()table.setPageSize(20)table.getCanNextPage()     // booleantable.getCanPreviousPage() // booleantable.getPageCount()       // total pages

Row Selection

typescript
const [rowSelection, setRowSelection] = useState<RowSelectionState>({})
const table = useReactTable({  state: { rowSelection },  onRowSelectionChange: setRowSelection,  enableRowSelection: true,  enableMultiRowSelection: true,})
// Checkbox columncolumnHelper.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 rowstable.getSelectedRowModel().rows

Column Visibility

typescript
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

typescript
const [columnPinning, setColumnPinning] = useState<ColumnPinningState>({  left: ['select', 'name'],  right: ['actions'],})
const table = useReactTable({  state: { columnPinning },  onColumnPinningChange: setColumnPinning,  enableColumnPinning: true,})
// Render pinned sections separatelyrow.getLeftVisibleCells()   // Left-pinnedrow.getCenterVisibleCells() // Unpinnedrow.getRightVisibleCells()  // Right-pinned

Column Resizing

typescript
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

typescript
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

typescript
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

typescript
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

typescript
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 stateuseEffect(() => {  fetchData({ sorting, filters: columnFilters, pagination })}, [sorting, columnFilters, pagination])

TypeScript Patterns

Extending Column Meta

typescript
declare module '@tanstack/react-table' {  interface ColumnMeta<TData extends RowData, TValue> {    filterVariant?: 'text' | 'range' | 'select'    align?: 'left' | 'center' | 'right'  }}

Custom Filter/Sort Function Registration

typescript
declare module '@tanstack/react-table' {  interface FilterFns {    fuzzy: FilterFn<unknown>  }  interface SortingFns {    myCustomSort: SortingFn<unknown>  }}

Editable Cells via Table Meta

typescript
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

typescript
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

  1. Always memoize data and columns to prevent infinite re-renders
  2. Use flexRender for all header/cell/footer rendering
  3. Use table.getRowModel().rows for final rendered rows (not getCoreRowModel)
  4. Import only needed row models - each adds processing to the pipeline
  5. Use getRowId for stable row keys when data has unique IDs
  6. Use manualX options for server-side operations
  7. Pair controlled state with both state.X and onXChange
  8. Use module augmentation for custom meta, filter fns, sort fns
  9. Use column helper for type-safe column definitions
  10. 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

来源与署名

来源:tanstack-skills/tanstack-skills位于plugins/tanstack-table/skills/tanstack-table提交6f5521e

许可证: 无许可证

内容归原作者所有。SourceWeft 从公开仓库中收录这些内容。

举报或申请下架