tanstack-table

Compare original and translation side by side

🇺🇸

Original

English
🇨🇳

Translation

Chinese

TanStack Table

TanStack Table

Headless data tables with server-side pagination, filtering, sorting, and virtualization for Cloudflare Workers + D1

为Cloudflare Workers + D1打造的支持服务端分页、筛选、排序和虚拟化的无头数据表格

Quick Start

快速开始

Last Updated: 2026-01-09 Versions: @tanstack/react-table@8.21.3, @tanstack/react-virtual@3.13.18
bash
npm install @tanstack/react-table@latest
npm install @tanstack/react-virtual@latest  # For virtualization
Basic Setup (CRITICAL: memoize data/columns to prevent infinite re-renders):
typescript
import { useReactTable, getCoreRowModel, ColumnDef } from '@tanstack/react-table'
import { useMemo } from 'react'

const columns: ColumnDef<User>[] = [
  { accessorKey: 'name', header: 'Name' },
  { accessorKey: 'email', header: 'Email' },
]

function UsersTable() {
  const data = useMemo(() => [...users], []) // Stable reference
  const table = useReactTable({ data, columns, getCoreRowModel: getCoreRowModel() })

  return (
    <table>
      <thead>
        {table.getHeaderGroups().map(group => (
          <tr key={group.id}>
            {group.headers.map(h => <th key={h.id}>{h.column.columnDef.header}</th>)}
          </tr>
        ))}
      </thead>
      <tbody>
        {table.getRowModel().rows.map(row => (
          <tr key={row.id}>
            {row.getVisibleCells().map(cell => <td key={cell.id}>{cell.renderValue()}</td>)}
          </tr>
        ))}
      </tbody>
    </table>
  )
}

最后更新: 2026-01-09 版本: @tanstack/react-table@8.21.3, @tanstack/react-virtual@3.13.18
bash
npm install @tanstack/react-table@latest
npm install @tanstack/react-virtual@latest  # For virtualization
基础配置(重要提示:对数据/列进行memo化处理以防止无限重渲染):
typescript
import { useReactTable, getCoreRowModel, ColumnDef } from '@tanstack/react-table'
import { useMemo } from 'react'

const columns: ColumnDef<User>[] = [
  { accessorKey: 'name', header: 'Name' },
  { accessorKey: 'email', header: 'Email' },
]

function UsersTable() {
  const data = useMemo(() => [...users], []) // Stable reference
  const table = useReactTable({ data, columns, getCoreRowModel: getCoreRowModel() })

  return (
    <table>
      <thead>
        {table.getHeaderGroups().map(group => (
          <tr key={group.id}>
            {group.headers.map(h => <th key={h.id}>{h.column.columnDef.header}</th>)}
          </tr>
        ))}
      </thead>
      <tbody>
        {table.getRowModel().rows.map(row => (
          <tr key={row.id}>
            {row.getVisibleCells().map(cell => <td key={cell.id}>{cell.renderValue()}</td>)}
          </tr>
        ))}
      </tbody>
    </table>
  )
}

Server-Side Patterns

服务端模式

Cloudflare D1 API (pagination + filtering + sorting):
typescript
// Workers API: functions/api/users.ts
export async function onRequestGet({ request, env }) {
  const url = new URL(request.url)
  const page = Number(url.searchParams.get('page')) || 0
  const pageSize = 20
  const search = url.searchParams.get('search') || ''
  const sortBy = url.searchParams.get('sortBy') || 'created_at'
  const sortOrder = url.searchParams.get('sortOrder') || 'DESC'

  const { results } = await env.DB.prepare(`
    SELECT * FROM users
    WHERE name LIKE ? OR email LIKE ?
    ORDER BY ${sortBy} ${sortOrder}
    LIMIT ? OFFSET ?
  `).bind(`%${search}%`, `%${search}%`, pageSize, page * pageSize).all()

  const { total } = await env.DB.prepare('SELECT COUNT(*) as total FROM users').first()

  return Response.json({
    data: results,
    pagination: { page, pageSize, total, pageCount: Math.ceil(total / pageSize) },
  })
}
Client-Side (TanStack Query + Table):
typescript
const [pagination, setPagination] = useState({ pageIndex: 0, pageSize: 20 })
const [columnFilters, setColumnFilters] = useState([])
const [sorting, setSorting] = useState([])

// CRITICAL: Include ALL state in query key
const { data, isLoading } = useQuery({
  queryKey: ['users', pagination, columnFilters, sorting],
  queryFn: async () => {
    const params = new URLSearchParams({
      page: pagination.pageIndex,
      search: columnFilters.find(f => f.id === 'search')?.value || '',
      sortBy: sorting[0]?.id || 'created_at',
      sortOrder: sorting[0]?.desc ? 'DESC' : 'ASC',
    })
    return fetch(`/api/users?${params}`).then(r => r.json())
  },
})

const table = useReactTable({
  data: data?.data ?? [],
  columns,
  getCoreRowModel: getCoreRowModel(),
  // CRITICAL: manual* flags tell table server handles these
  manualPagination: true,
  manualFiltering: true,
  manualSorting: true,
  pageCount: data?.pagination.pageCount ?? 0,
  state: { pagination, columnFilters, sorting },
  onPaginationChange: setPagination,
  onColumnFiltersChange: setColumnFilters,
  onSortingChange: setSorting,
})

Cloudflare D1 API(分页 + 筛选 + 排序):
typescript
// Workers API: functions/api/users.ts
export async function onRequestGet({ request, env }) {
  const url = new URL(request.url)
  const page = Number(url.searchParams.get('page')) || 0
  const pageSize = 20
  const search = url.searchParams.get('search') || ''
  const sortBy = url.searchParams.get('sortBy') || 'created_at'
  const sortOrder = url.searchParams.get('sortOrder') || 'DESC'

  const { results } = await env.DB.prepare(`
    SELECT * FROM users
    WHERE name LIKE ? OR email LIKE ?
    ORDER BY ${sortBy} ${sortOrder}
    LIMIT ? OFFSET ?
  `).bind(`%${search}%`, `%${search}%`, pageSize, page * pageSize).all()

  const { total } = await env.DB.prepare('SELECT COUNT(*) as total FROM users').first()

  return Response.json({
    data: results,
    pagination: { page, pageSize, total, pageCount: Math.ceil(total / pageSize) },
  })
}
客户端(TanStack Query + 表格):
typescript
const [pagination, setPagination] = useState({ pageIndex: 0, pageSize: 20 })
const [columnFilters, setColumnFilters] = useState([])
const [sorting, setSorting] = useState([])

// CRITICAL: Include ALL state in query key
const { data, isLoading } = useQuery({
  queryKey: ['users', pagination, columnFilters, sorting],
  queryFn: async () => {
    const params = new URLSearchParams({
      page: pagination.pageIndex,
      search: columnFilters.find(f => f.id === 'search')?.value || '',
      sortBy: sorting[0]?.id || 'created_at',
      sortOrder: sorting[0]?.desc ? 'DESC' : 'ASC',
    })
    return fetch(`/api/users?${params}`).then(r => r.json())
  },
})

const table = useReactTable({
  data: data?.data ?? [],
  columns,
  getCoreRowModel: getCoreRowModel(),
  // CRITICAL: manual* flags tell table server handles these
  manualPagination: true,
  manualFiltering: true,
  manualSorting: true,
  pageCount: data?.pagination.pageCount ?? 0,
  state: { pagination, columnFilters, sorting },
  onPaginationChange: setPagination,
  onColumnFiltersChange: setColumnFilters,
  onSortingChange: setSorting,
})

Virtualization (1000+ Rows)

虚拟化(1000+行数据)

Render only visible rows for performance:
typescript
import { useVirtualizer } from '@tanstack/react-virtual'

function VirtualizedTable() {
  const containerRef = useRef<HTMLDivElement>(null)
  const table = useReactTable({ data: largeDataset, columns, getCoreRowModel: getCoreRowModel() })
  const { rows } = table.getRowModel()

  const rowVirtualizer = useVirtualizer({
    count: rows.length,
    getScrollElement: () => containerRef.current,
    estimateSize: () => 50, // Row height px
    overscan: 10,
  })

  return (
    <div ref={containerRef} style={{ height: '600px', overflow: 'auto' }}>
      <table style={{ height: `${rowVirtualizer.getTotalSize()}px` }}>
        <tbody>
          {rowVirtualizer.getVirtualItems().map(virtualRow => {
            const row = rows[virtualRow.index]
            return (
              <tr key={row.id} style={{ position: 'absolute', transform: `translateY(${virtualRow.start}px)` }}>
                {row.getVisibleCells().map(cell => <td key={cell.id}>{cell.renderValue()}</td>)}
              </tr>
            )
          })}
        </tbody>
      </table>
    </div>
  )
}
仅渲染可见行以提升性能:
typescript
import { useVirtualizer } from '@tanstack/react-virtual'

function VirtualizedTable() {
  const containerRef = useRef<HTMLDivElement>(null)
  const table = useReactTable({ data: largeDataset, columns, getCoreRowModel: getCoreRowModel() })
  const { rows } = table.getRowModel()

  const rowVirtualizer = useVirtualizer({
    count: rows.length,
    getScrollElement: () => containerRef.current,
    estimateSize: () => 50, // Row height px
    overscan: 10,
  })

  return (
    <div ref={containerRef} style={{ height: '600px', overflow: 'auto' }}>
      <table style={{ height: `${rowVirtualizer.getTotalSize()}px` }}>
        <tbody>
          {rowVirtualizer.getVirtualItems().map(virtualRow => {
            const row = rows[virtualRow.index]
            return (
              <tr key={row.id} style={{ position: 'absolute', transform: `translateY(${virtualRow.start}px)` }}>
                {row.getVisibleCells().map(cell => <td key={cell.id}>{cell.renderValue()}</td>)}
              </tr>
            )
          })}
        </tbody>
      </table>
    </div>
  )
}

Warning: Hidden Containers (Tabs/Modals)

注意:隐藏容器(标签页/模态框)

Known Issue: When using virtualization inside tabbed content or modals that hide inactive content with
display: none
, the virtualizer continues performing layout calculations while hidden, causing:
  • Infinite re-render loops (large datasets: 50k+ rows)
  • Incorrect scroll position when tab becomes visible
  • Empty table or reset scroll (small datasets)
Prevention:
typescript
const rowVirtualizer = useVirtualizer({
  count: rows.length,
  getScrollElement: () => containerRef.current,
  estimateSize: () => 50,
  overscan: 10,
  // Disable when container is hidden to prevent infinite re-renders
  enabled: containerRef.current?.getClientRects().length !== 0,
})

// OR: Conditionally render instead of hiding with CSS
{isVisible && <VirtualizedTable />}

已知问题:当在使用
display: none
隐藏非活跃内容的标签页或模态框内使用虚拟化时,虚拟化器会在隐藏状态下继续执行布局计算,导致:
  • 无限重渲染循环(大数据集:50k+行)
  • 标签页显示时滚动位置不正确
  • 表格为空或滚动位置重置(小数据集)
解决方法
typescript
const rowVirtualizer = useVirtualizer({
  count: rows.length,
  getScrollElement: () => containerRef.current,
  estimateSize: () => 50,
  overscan: 10,
  // 容器隐藏时禁用虚拟化以防止无限重渲染
  enabled: containerRef.current?.getClientRects().length !== 0,
})

// 或者:通过条件渲染替代CSS隐藏
{isVisible && <VirtualizedTable />}

Column/Row Pinning

列/行固定

Pin columns or rows to keep them visible during horizontal/vertical scroll:
typescript
import { useReactTable, getCoreRowModel } from '@tanstack/react-table'

const table = useReactTable({
  data,
  columns,
  getCoreRowModel: getCoreRowModel(),
  // Enable pinning
  enableColumnPinning: true,
  enableRowPinning: true,
  // Initial pinning state
  initialState: {
    columnPinning: {
      left: ['select', 'name'],  // Pin to left
      right: ['actions'],        // Pin to right
    },
  },
})

// Render with pinned columns
function PinnedTable() {
  return (
    <div className="flex">
      {/* Left pinned columns */}
      <div className="sticky left-0 bg-background z-10">
        {table.getLeftHeaderGroups().map(/* render left headers */)}
        {table.getRowModel().rows.map(row => (
          <tr>{row.getLeftVisibleCells().map(/* render cells */)}</tr>
        ))}
      </div>

      {/* Center scrollable columns */}
      <div className="overflow-x-auto">
        {table.getCenterHeaderGroups().map(/* render center headers */)}
        {table.getRowModel().rows.map(row => (
          <tr>{row.getCenterVisibleCells().map(/* render cells */)}</tr>
        ))}
      </div>

      {/* Right pinned columns */}
      <div className="sticky right-0 bg-background z-10">
        {table.getRightHeaderGroups().map(/* render right headers */)}
        {table.getRowModel().rows.map(row => (
          <tr>{row.getRightVisibleCells().map(/* render cells */)}</tr>
        ))}
      </div>
    </div>
  )
}

// Toggle pinning programmatically
column.pin('left')   // Pin column to left
column.pin('right')  // Pin column to right
column.pin(false)    // Unpin column
row.pin('top')       // Pin row to top
row.pin('bottom')    // Pin row to bottom
固定列或行,使其在水平/垂直滚动时保持可见:
typescript
import { useReactTable, getCoreRowModel } from '@tanstack/react-table'

const table = useReactTable({
  data,
  columns,
  getCoreRowModel: getCoreRowModel(),
  // 启用固定功能
  enableColumnPinning: true,
  enableRowPinning: true,
  // 初始固定状态
  initialState: {
    columnPinning: {
      left: ['select', 'name'],  // 固定到左侧
      right: ['actions'],        // 固定到右侧
    },
  },
})

// 渲染带固定列的表格
function PinnedTable() {
  return (
    <div className="flex">
      {/* 左侧固定列 */}
      <div className="sticky left-0 bg-background z-10">
        {table.getLeftHeaderGroups().map(/* 渲染左侧表头 */)}
        {table.getRowModel().rows.map(row => (
          <tr>{row.getLeftVisibleCells().map(/* 渲染单元格 */)}</tr>
        ))}
      </div>

      {/* 中间可滚动列 */}
      <div className="overflow-x-auto">
        {table.getCenterHeaderGroups().map(/* 渲染中间表头 */)}
        {table.getRowModel().rows.map(row => (
          <tr>{row.getCenterVisibleCells().map(/* 渲染单元格 */)}</tr>
        ))}
      </div>

      {/* 右侧固定列 */}
      <div className="sticky right-0 bg-background z-10">
        {table.getRightHeaderGroups().map(/* 渲染右侧表头 */)}
        {table.getRowModel().rows.map(row => (
          <tr>{row.getRightVisibleCells().map(/* 渲染单元格 */)}</tr>
        ))}
      </div>
    </div>
  )
}

// 以编程方式切换固定状态
column.pin('left')   // 将列固定到左侧
column.pin('right')  // 将列固定到右侧
column.pin(false)    // 取消列固定
row.pin('top')       // 将行固定到顶部
row.pin('bottom')    // 将行固定到底部

Warning: Column Pinning with Column Groups

注意:列分组与列固定

Known Issue: Pinning parent group columns (created with
columnHelper.group()
) causes incorrect positioning and duplicated headers.
column.getStart('left')
returns wrong values for group headers.
Prevention:
typescript
// Disable pinning for grouped columns
const isPinnable = (column) => !column.parent

// OR: Pin individual columns within group, not the group itself
table.getColumn('firstName')?.pin('left')
table.getColumn('lastName')?.pin('left')
// Don't pin the parent group column

已知问题:固定父级分组列(通过
columnHelper.group()
创建)会导致定位错误和表头重复。分组表头的
column.getStart('left')
返回值不正确。
解决方法
typescript
// 禁用分组列的固定功能
const isPinnable = (column) => !column.parent

// 或者:固定分组内的单个列,而非父级分组列
table.getColumn('firstName')?.pin('left')
table.getColumn('lastName')?.pin('left')
// 不要固定父级分组列

Row Expanding (Nested Data)

行展开(嵌套数据)

Show/hide child rows or additional details:
typescript
import { useReactTable, getCoreRowModel, getExpandedRowModel } from '@tanstack/react-table'

// Data with nested children
const data = [
  {
    id: 1,
    name: 'Parent Row',
    subRows: [
      { id: 2, name: 'Child Row 1' },
      { id: 3, name: 'Child Row 2' },
    ],
  },
]

const table = useReactTable({
  data,
  columns,
  getCoreRowModel: getCoreRowModel(),
  getExpandedRowModel: getExpandedRowModel(),  // Required for expanding
  getSubRows: row => row.subRows,               // Tell table where children are
})

// Render with expand button
function ExpandableTable() {
  return (
    <tbody>
      {table.getRowModel().rows.map(row => (
        <>
          <tr key={row.id}>
            <td>
              {row.getCanExpand() && (
                <button onClick={row.getToggleExpandedHandler()}>
                  {row.getIsExpanded() ? '▼' : '▶'}
                </button>
              )}
            </td>
            {row.getVisibleCells().map(cell => (
              <td key={cell.id} style={{ paddingLeft: `${row.depth * 20}px` }}>
                {cell.renderValue()}
              </td>
            ))}
          </tr>
        </>
      ))}
    </tbody>
  )
}

// Control expansion programmatically
table.toggleAllRowsExpanded()     // Expand/collapse all
row.toggleExpanded()              // Toggle single row
table.getIsAllRowsExpanded()      // Check if all expanded
Detail Rows (custom content, not nested data):
typescript
function DetailRow({ row }) {
  if (!row.getIsExpanded()) return null

  return (
    <tr>
      <td colSpan={columns.length}>
        <div className="p-4 bg-muted">
          Custom detail content for row {row.id}
        </div>
      </td>
    </tr>
  )
}

显示/隐藏子行或额外详情:
typescript
import { useReactTable, getCoreRowModel, getExpandedRowModel } from '@tanstack/react-table'

// 带嵌套子行的数据
const data = [
  {
    id: 1,
    name: 'Parent Row',
    subRows: [
      { id: 2, name: 'Child Row 1' },
      { id: 3, name: 'Child Row 2' },
    ],
  },
]

const table = useReactTable({
  data,
  columns,
  getCoreRowModel: getCoreRowModel(),
  getExpandedRowModel: getExpandedRowModel(),  // 行展开功能所需
  getSubRows: row => row.subRows,               // 告诉表格子行的位置
})

// 带展开按钮的渲染
function ExpandableTable() {
  return (
    <tbody>
      {table.getRowModel().rows.map(row => (
        <>
          <tr key={row.id}>
            <td>
              {row.getCanExpand() && (
                <button onClick={row.getToggleExpandedHandler()}>
                  {row.getIsExpanded() ? '▼' : '▶'}
                </button>
              )}
            </td>
            {row.getVisibleCells().map(cell => (
              <td key={cell.id} style={{ paddingLeft: `${row.depth * 20}px` }}>
                {cell.renderValue()}
              </td>
            ))}
          </tr>
        </>
      ))}
    </tbody>
  )
}

// 以编程方式控制展开状态
table.toggleAllRowsExpanded()     // 展开/折叠所有行
row.toggleExpanded()              // 切换单行展开状态
table.getIsAllRowsExpanded()      // 检查是否所有行都已展开
详情行(自定义内容,非嵌套数据):
typescript
function DetailRow({ row }) {
  if (!row.getIsExpanded()) return null

  return (
    <tr>
      <td colSpan={columns.length}>
        <div className="p-4 bg-muted">
{row.id} 的自定义详情内容
        </div>
      </td>
    </tr>
  )
}

Row Grouping

行分组

Group rows by column values:
typescript
import { useReactTable, getCoreRowModel, getGroupedRowModel } from '@tanstack/react-table'

const table = useReactTable({
  data,
  columns,
  getCoreRowModel: getCoreRowModel(),
  getGroupedRowModel: getGroupedRowModel(),    // Required for grouping
  getExpandedRowModel: getExpandedRowModel(),  // Groups are expandable
  initialState: {
    grouping: ['status'],  // Group by 'status' column
  },
})

// Column with aggregation
const columns = [
  {
    accessorKey: 'status',
    header: 'Status',
  },
  {
    accessorKey: 'amount',
    header: 'Amount',
    aggregationFn: 'sum',                      // Sum grouped values
    aggregatedCell: ({ getValue }) => `Total: ${getValue()}`,
  },
]

// Render grouped table
function GroupedTable() {
  return (
    <tbody>
      {table.getRowModel().rows.map(row => (
        <tr key={row.id}>
          {row.getVisibleCells().map(cell => (
            <td key={cell.id}>
              {cell.getIsGrouped() ? (
                // Grouped cell - show group header with expand toggle
                <button onClick={row.getToggleExpandedHandler()}>
                  {row.getIsExpanded() ? '▼' : '▶'} {cell.renderValue()} ({row.subRows.length})
                </button>
              ) : cell.getIsAggregated() ? (
                // Aggregated cell - show aggregation result
                cell.renderValue()
              ) : cell.getIsPlaceholder() ? null : (
                // Regular cell
                cell.renderValue()
              )}
            </td>
          ))}
        </tr>
      ))}
    </tbody>
  )
}

// Built-in aggregation functions
// 'sum', 'min', 'max', 'extent', 'mean', 'median', 'unique', 'uniqueCount', 'count'
按列值对行进行分组:
typescript
import { useReactTable, getCoreRowModel, getGroupedRowModel } from '@tanstack/react-table'

const table = useReactTable({
  data,
  columns,
  getCoreRowModel: getCoreRowModel(),
  getGroupedRowModel: getGroupedRowModel(),    // 行分组功能所需
  getExpandedRowModel: getExpandedRowModel(),  // 分组支持展开
  initialState: {
    grouping: ['status'],  // 按'status'列分组
  },
})

// 带聚合功能的列
const columns = [
  {
    accessorKey: 'status',
    header: 'Status',
  },
  {
    accessorKey: 'amount',
    header: 'Amount',
    aggregationFn: 'sum',                      // 对分组值求和
    aggregatedCell: ({ getValue }) => `Total: ${getValue()}`,
  },
]

// 渲染分组表格
function GroupedTable() {
  return (
    <tbody>
      {table.getRowModel().rows.map(row => (
        <tr key={row.id}>
          {row.getVisibleCells().map(cell => (
            <td key={cell.id}>
              {cell.getIsGrouped() ? (
                // 分组单元格 - 显示分组表头及展开切换按钮
                <button onClick={row.getToggleExpandedHandler()}>
                  {row.getIsExpanded() ? '▼' : '▶'} {cell.renderValue()} ({row.subRows.length})
                </button>
              ) : cell.getIsAggregated() ? (
                // 聚合单元格 - 显示聚合结果
                cell.renderValue()
              ) : cell.getIsPlaceholder() ? null : (
                // 普通单元格
                cell.renderValue()
              )}
            </td>
          ))}
        </tr>
      ))}
    </tbody>
  )
}

// 内置聚合函数
// 'sum', 'min', 'max', 'extent', 'mean', 'median', 'unique', 'uniqueCount', 'count'

Warning: Performance Bottleneck with Grouping (Community-sourced)

注意:分组功能的性能瓶颈(社区反馈)

Known Issue: The grouping feature causes significant performance degradation on medium-to-large datasets. With grouping enabled, render times can increase from <1 second to 30-40 seconds on 50k rows due to excessive memory usage in
createRow
calculations.
Verified: Community testing + GitHub issue report
Prevention:
typescript
// 1. Use server-side grouping for large datasets
// 2. Implement pagination to limit rows per page
// 3. Disable grouping for 10k+ rows
const shouldEnableGrouping = data.length < 10000

// 4. OR: Use React.memo on row components
const MemoizedRow = React.memo(TableRow)

已知问题:分组功能会导致中等至大型数据集的性能显著下降。启用分组后,50k行数据的渲染时间可能从<1秒增加到30-40秒,原因是
createRow
计算中的内存占用过高。
已验证:社区测试 + GitHub问题报告
解决方法
typescript
// 1. 对大数据集使用服务端分组
// 2. 实现分页以限制每页行数
// 3. 对10k+行数据禁用分组
const shouldEnableGrouping = data.length < 10000

// 4. 或者:对行组件使用React.memo
const MemoizedRow = React.memo(TableRow)

Known Issues & Solutions

已知问题与解决方案

Issue #1: Infinite Re-Renders
  • Error: Table re-renders infinitely, browser freezes
  • Cause:
    data
    or
    columns
    references change on every render
  • Fix: Use
    useMemo(() => [...], [])
    or define data/columns outside component
Issue #2: Query + Table State Mismatch
  • Error: Query refetches but pagination state not synced, stale data
  • Cause: Query key missing table state (pagination, filters, sorting)
  • Fix: Include ALL state in query key:
    queryKey: ['users', pagination, columnFilters, sorting]
Issue #3: Server-Side Features Not Working
  • Error: Pagination/filtering/sorting doesn't trigger API calls
  • Cause: Missing
    manual*
    flags
  • Fix: Set
    manualPagination: true
    ,
    manualFiltering: true
    ,
    manualSorting: true
    + provide
    pageCount
Issue #4: TypeScript "Cannot Find Module"
  • Error: Import errors for
    createColumnHelper
  • Fix: Import from
    @tanstack/react-table
    (NOT
    @tanstack/table-core
    )
Issue #5: Sorting Not Working Server-Side
  • Error: Clicking sort headers doesn't update data
  • Cause: Sorting state not in query key/API params
  • Fix: Include
    sorting
    in query key, add sort params to API call, set
    manualSorting: true
    +
    onSortingChange
Issue #6: Poor Performance (1000+ Rows)
  • Error: Table slow/laggy with large datasets
  • Fix: Use TanStack Virtual for client-side OR implement server-side pagination
Issue #7: React Compiler Incompatibility (React 19+)
  • Error:
    "Table doesn't re-render when data changes"
    (with React Compiler enabled)
  • Source: GitHub Issue #5567
  • Why It Happens: React Compiler's automatic memoization conflicts with table core instance, preventing re-renders when data/state changes
  • Prevention: Add
    "use no memo"
    directive at top of components using
    useReactTable
    :
typescript
"use no memo"

function TableComponent() {
  const table = useReactTable({ data, columns, getCoreRowModel: getCoreRowModel() })
  // Now works correctly with React Compiler
}
Note: This issue also affects column visibility and row selection. Full fix coming in v9.
Issue #8: Server-Side Pagination Row Selection Bug
  • Error:
    toggleAllRowsSelected(false)
    only deselects current page, not all pages
  • Source: GitHub Issue #5929
  • Why It Happens: Selection state persists across pages (intentional for server-side use cases), but header checkbox state is calculated incorrectly
  • Prevention: Manually clear selection state when toggling off:
typescript
const toggleAllRows = (value: boolean) => {
  if (!value) {
    table.setRowSelection({}) // Clear entire selection object
  } else {
    table.toggleAllRowsSelected(true)
  }
}
Issue #9: Client-Side onPaginationChange Returns Incorrect pageIndex
  • Error:
    onPaginationChange
    always returns
    pageIndex: 0
    instead of current page
  • Source: GitHub Issue #5970
  • Why It Happens: Client-side pagination mode has state tracking bug (only occurs in client mode, works correctly in server/manual mode)
  • Prevention: Switch to manual pagination for correct behavior:
typescript
// Instead of relying on client-side pagination
const table = useReactTable({
  data,
  columns,
  manualPagination: true, // Forces correct state tracking
  pageCount: Math.ceil(data.length / pagination.pageSize),
  state: { pagination },
  onPaginationChange: setPagination,
})
Issue #10: Row Selection Not Cleaned Up When Data Removed
  • Error: Selected rows that no longer exist in data remain in selection state
  • Source: GitHub Issue #5850
  • Why It Happens: Intentional behavior to support server-side pagination (where rows disappear from current page but should stay selected)
  • Prevention: Manually clean up selection when removing data:
typescript
const removeRow = (idToRemove: string) => {
  // Remove from data
  setData(data.filter(row => row.id !== idToRemove))

  // Clean up selection if it was selected
  const { rowSelection } = table.getState()
  if (rowSelection[idToRemove]) {
    table.setRowSelection((old) => {
      const filtered = Object.entries(old).filter(([id]) => id !== idToRemove)
      return Object.fromEntries(filtered)
    })
  }
}

// OR: Use table.resetRowSelection(true) to clear all
Issue #11: Performance Degradation with React DevTools Open
  • Error: Table performance significantly degrades with React DevTools open (development only)
  • Why It Happens: DevTools inspects table instance and row models on every render, especially noticeable with 500+ rows
  • Fix: Close React DevTools during performance testing. This is not a production issue.
Issue #12: TypeScript getValue() Type Inference with Grouped Columns
  • Error:
    getValue()
    returns
    unknown
    instead of accessor's actual type inside
    columnHelper.group()
  • Source: GitHub Issue #5860
  • Fix: Manually specify type or use
    renderValue()
    :
typescript
// Option 1: Type assertion
cell: (info) => {
  const value = info.getValue() as string
  return value.toUpperCase()
}

// Option 2: Use renderValue() (better type inference)
cell: (info) => {
  const value = info.renderValue()
  return typeof value === 'string' ? value.toUpperCase() : value
}

Related Skills: tanstack-query (data fetching), cloudflare-d1 (database backend), tailwind-v4-shadcn (UI styling)

Last verified: 2026-01-21 | Skill version: 2.0.0 | Changes: Added 7 new known issues from TIER 1-2 research findings (React 19 Compiler, server-side row selection, virtualization in hidden containers, client-side pagination bug, column pinning with groups, row selection cleanup, DevTools performance, TypeScript getValue). Error count: 6 → 12.
问题1:无限重渲染
  • 错误表现:表格无限重渲染,浏览器冻结
  • 原因
    data
    columns
    的引用在每次渲染时发生变化
  • 解决方法:使用
    useMemo(() => [...], [])
    或将数据/列定义在组件外部
问题2:Query与表格状态不匹配
  • 错误表现:Query重新获取数据但分页状态未同步,显示过期数据
  • 原因:Query键中缺少表格状态(分页、筛选、排序)
  • 解决方法:将所有状态包含在Query键中:
    queryKey: ['users', pagination, columnFilters, sorting]
问题3:服务端功能无法工作
  • 错误表现:分页/筛选/排序无法触发API调用
  • 原因:缺少
    manual*
    标志
  • 解决方法:设置
    manualPagination: true
    manualFiltering: true
    manualSorting: true
    并提供
    pageCount
问题4:TypeScript "无法找到模块"
  • 错误表现:导入
    createColumnHelper
    时出错
  • 解决方法:从
    @tanstack/react-table
    导入(而非
    @tanstack/table-core
问题5:服务端排序无法工作
  • 错误表现:点击排序表头后数据未更新
  • 原因:排序状态未包含在Query键/API参数中
  • 解决方法:将
    sorting
    加入Query键,在API调用中添加排序参数,设置
    manualSorting: true
    onSortingChange
问题6:性能不佳(1000+行)
  • 错误表现:大数据集下表格缓慢/卡顿
  • 解决方法:使用TanStack Virtual实现客户端虚拟化,或实现服务端分页
问题7:React编译器兼容性问题(React 19+)
  • 错误表现:"数据变化时表格不重渲染"(启用React Compiler时)
  • 来源GitHub Issue #5567
  • 原因:React Compiler的自动memo化与表格核心实例冲突,导致数据/状态变化时无法触发重渲染
  • 解决方法:在使用
    useReactTable
    的组件顶部添加
    "use no memo"
    指令:
typescript
"use no memo"

function TableComponent() {
  const table = useReactTable({ data, columns, getCoreRowModel: getCoreRowModel() })
  // 现在可与React Compiler正常兼容
}
注意:此问题同样影响列可见性和行选择功能。完整修复将在v9版本中推出。
问题8:服务端分页下的行选择Bug
  • 错误表现
    toggleAllRowsSelected(false)
    仅取消当前页的选择,而非所有页
  • 来源GitHub Issue #5929
  • 原因:选择状态在页面间持久化(服务端场景下的设计),但表头复选框状态计算错误
  • 解决方法:取消选择时手动清空选择状态:
typescript
const toggleAllRows = (value: boolean) => {
  if (!value) {
    table.setRowSelection({}) // 清空整个选择对象
  } else {
    table.toggleAllRowsSelected(true)
  }
}
问题9:客户端onPaginationChange返回错误的pageIndex
  • 错误表现
    onPaginationChange
    始终返回
    pageIndex: 0
    而非当前页
  • 来源GitHub Issue #5970
  • 原因:客户端分页模式存在状态跟踪Bug(仅在客户端模式下出现,服务端/手动模式正常)
  • 解决方法:切换为手动分页以获得正确行为:
typescript
// 替代依赖客户端分页
const table = useReactTable({
  data,
  columns,
  manualPagination: true, // 强制正确的状态跟踪
  pageCount: Math.ceil(data.length / pagination.pageSize),
  state: { pagination },
  onPaginationChange: setPagination,
})
问题10:数据删除后行选择状态未清理
  • 错误表现:已从数据中移除的行仍保留在选择状态中
  • 来源GitHub Issue #5850
  • 原因:此为服务端分页场景下的设计行为(行从当前页消失但应保持选中状态)
  • 解决方法:删除数据时手动清理选择状态:
typescript
const removeRow = (idToRemove: string) => {
  // 从数据中移除
  setData(data.filter(row => row.id !== idToRemove))

  // 若该行已被选中,则清理选择状态
  const { rowSelection } = table.getState()
  if (rowSelection[idToRemove]) {
    table.setRowSelection((old) => {
      const filtered = Object.entries(old).filter(([id]) => id !== idToRemove)
      return Object.fromEntries(filtered)
    })
  }
}

// 或者:使用table.resetRowSelection(true)清空所有选择
问题11:打开React DevTools时性能下降
  • 错误表现:打开React DevTools时表格性能显著下降(仅开发环境)
  • 原因:DevTools在每次渲染时检查表格实例和行模型,500+行数据时尤为明显
  • 解决方法:性能测试时关闭React DevTools。此问题不会出现在生产环境。
问题12:分组列中TypeScript getValue()类型推断错误
  • 错误表现
    columnHelper.group()
    内的
    getValue()
    返回
    unknown
    而非访问器的实际类型
  • 来源GitHub Issue #5860
  • 解决方法:手动指定类型或使用
    renderValue()
typescript
// 选项1:类型断言
cell: (info) => {
  const value = info.getValue() as string
  return value.toUpperCase()
}

// 选项2:使用renderValue()(类型推断更优)
cell: (info) => {
  const value = info.renderValue()
  return typeof value === 'string' ? value.toUpperCase() : value
}

相关技能:tanstack-query(数据获取)、cloudflare-d1(数据库后端)、tailwind-v4-shadcn(UI样式)

最后验证:2026-01-21 | 技能版本:2.0.0 | 变更:根据TIER 1-2研究结果新增7个已知问题(React 19编译器、服务端行选择、隐藏容器内的虚拟化、客户端分页Bug、分组列固定、行选择清理、DevTools性能、TypeScript getValue)。错误数量:6 → 12。