TanStack Table 基础

要点

表格的 data 是原始记录,列定义描述如何读值和渲染。排序、筛选和分页应明确是在当前数据上计算还是由服务端完成。

核心三角

TanStack Table 一切围绕三个概念:

  1. data:TData[],你的原始数组(通常来自 Query)
  2. columns:ColumnDef 数组,列的声明(怎么取值、怎么渲染表头/单元格;v9 泛型为 ColumnDef<TFeatures, TData, TValue>,推荐用 createColumnHelper<TFeatures, TData>() 保持泛型一致)
  3. table:useTable 返回的实例,持有全部状态与行模型

定义: 列模型把”数据字段”与”渲染方式”解耦——accessor 负责取值,cell 负责渲染,二者互不干扰。

实践:渲染第一张表格

步骤一:声明数据与列

import { useMemo } from 'react'
import {
  createColumnHelper,
  flexRender,
  tableFeatures,
  useTable,
} from '@tanstack/react-table'

type Person = {
  id: number
  firstName: string
  lastName: string
  age: number
  score: number
}

// v9:createColumnHelper 需要两个泛型——TFeatures 与 TData;核心特性会自动合并,无需显式注册
// 以下是模块顶层的纯常量:features 与 columnHelper 无组件依赖,放组件外避免重建;
// 但 useMemo 只能在组件/Hook 内调用,columns 若要记忆化必须移进组件(见步骤二)
const features = tableFeatures({})
const columnHelper = createColumnHelper<typeof features, Person>()

const buildColumns = () =>
  columnHelper.columns([
    // 简单取值:accessorKey 直接读字段
    { accessorKey: 'firstName', header: '名' },
    { accessorKey: 'lastName', header: '姓' },
    { accessorKey: 'age', header: '年龄' },
    // 自定义取值:accessorFn 派生数据 + 自定义渲染
    {
      id: 'fullName',
      accessorFn: (row) => `${row.lastName}${row.firstName}`,
      header: '全名',
      cell: (info) => <strong>{info.getValue<string>()}</strong>,
    },
    {
      accessorKey: 'score',
      header: '评分',
      cell: (info) => {
        const score = info.getValue<number>()
        return <span>{score >= 60 ? `✅ ${score}` : `❌ ${score}`}</span>
      },
    },
  ])

步骤二:创建 table 实例并渲染

function PersonTable({ data }: { data: Person[] }) {
  // 列定义在组件内用 useMemo 记忆化(模块顶层不能调用 Hook)
  const columns = useMemo(buildColumns, [])

  // v9:features 是必填入口,核心特性(table/column/row/header/cell 与核心行模型行为)会自动合并进来,无需显式注册
  const table = useTable({
    data, // 建议用 useMemo 稳定引用,见下方"陷阱"
    columns,
    features, // 复用上方特性集;后续按需在此注册排序/筛选等特性
  })

  return (
    <table className="w-full border-collapse text-sm">
      <thead>
        {table.getHeaderGroups().map((headerGroup) => (
          <tr key={headerGroup.id}>
            {headerGroup.headers.map((header) => (
              <th key={header.id} className="border-b px-3 py-2 text-left">
                {/* flexRender 会同时处理纯文本与 JSX 两种 header */}
                {header.isPlaceholder
                  ? null
                  : flexRender(header.column.columnDef.header, header.getContext())}
              </th>
            ))}
          </tr>
        ))}
      </thead>
      <tbody>
        {table.getRowModel().rows.map((row) => (
          <tr key={row.id} className="hover:bg-gray-50">
            {row.getAllCells().map((cell) => (
              <td key={cell.id} className="border-b px-3 py-2">
                {flexRender(cell.column.columnDef.cell, cell.getContext())}
              </td>
            ))}
          </tr>
        ))}
      </tbody>
    </table>
  )
}

关键点解析:

  • v9 中 features 为必填项,核心特性(table/column/row/header/cell 与核心行模型行为)会自动合并进来,无需显式注册——它把 data + columns 管道化成可渲染的行;排序/筛选等其余特性按需注册到 tableFeatures()
  • header / cell 既可以是字符串,也可以是返回 JSX 的函数,flexRender 统一处理两种形态
  • info.getValue() 返回 accessor 的结果;row.original 可拿到整行原始数据

行模型流水线

行模型是 Table v9 的灵魂:数据像流水线一样被逐层加工。

data → 核心行模型 → 排序 → 筛选 → 分组 → 展开树 → 分页 → table.getRowModel()

你按需”接通”管道(v9 中特性与行模型槽位都要注册到 tableFeatures() 上),不接的环节自动跳过:

// 自定义排序函数(如中文按拼音)注册进 sortFns,列上用 sortingFn: 'pinyin' 引用
const sortFns = {
  pinyin: (a, b, columnId) => String(a.getValue(columnId)).localeCompare(String(b.getValue(columnId)), 'zh')
}
const filterFns = {
  includes: (row, columnId, filterValue) => String(row.getValue(columnId)).includes(filterValue)
}

const table = useTable({
  data,
  columns,
  features: tableFeatures({
    rowSortingFeature,                          // 先注册特性(核心特性自动合并,无需显式注册)
    columnFilteringFeature,
    sortedRowModel: createSortedRowModel(),     // 再接通排序行模型
    filteredRowModel: createFilteredRowModel(), // 再接通筛选行模型
    sortFns,                                    // 自定义排序函数表
    filterFns,                                  // 自定义筛选函数表
  }),
})

各环节的完整参数与 API 见 TanStack Table 官方文档与 v8 到 v9 迁移指南。

最佳实践

表格先从 data 和 columns 建立行、列的含义,再由行模型应用排序、筛选与分页;如果直接遍历原始 data 渲染,就可能绕过这些处理,出现“按钮变了,行顺序没变”。改变排序后检查屏幕行序,是比“已注册功能”更直接的验收。

不要在每次渲染时无条件创建相同的派生数组,导致表格重复处理;可将固定列放到组件外,把昂贵派生计算按实际依赖记忆化。Query 对符合条件的数据会做结构共享,但不是所有 data 永远引用相同。列身份要稳定,无法从 accessor 或标题推导时显式设置 id;功能注册方式按本文对应 Table 版本核对,不能把某版本配置当通用 API。


练习与实践

练习一:基础渲染

  • [ ] 用 jsonplaceholder 的 /users 接口作为 data,渲染姓名/邮箱/公司三列
  • [ ] 给”邮箱”列加 cell 自定义渲染,用 <a> 链接展示

练习二:接入 Query

  • [ ] 用 useQuery 取数并渲染表格,完整跑通”Query → Table”链路
  • [ ] 加载中时渲染骨架占位行(提示:判断 isPending)


延伸阅读

暂无评论

发送评论 编辑评论

|´・ω・)ノ
ヾ(≧∇≦*)ゝ
(☆ω☆)
(╯‵□′)╯︵┴─┴
 ̄﹃ ̄
(/ω\)
∠( ᐛ 」∠)_
(๑•̀ㅁ•́ฅ)
→_→
୧(๑•̀⌄•́๑)૭
٩(ˊᗜˋ*)و
(ノ°ο°)ノ
(´இ皿இ`)
⌇●﹏●⌇
(ฅ´ω`ฅ)
(╯°A°)╯︵○○○
φ( ̄∇ ̄o)
ヾ(´・ ・`。)ノ"
( ง ᵒ̌皿ᵒ̌)ง⁼³₌₃
(ó﹏ò。)
Σ(っ °Д °;)っ
( ,,´・ω・)ノ"(´っω・`。)
╮(╯▽╰)╭
o(*////▽////*)q
>﹏<
( ๑´•ω•) "(ㆆᴗㆆ)
😂
😀
😅
😊
🙂
🙃
😌
😍
😘
😜
😝
😏
😒
🙄
😳
😡
😔
😫
😱
😭
💩
👻
🙌
🖕
👍
👫
👬
👭
🌚
🌝
🙈
💊
😶
🙏
🍦
🍉
😣
Source: github.com/k4yt3x/flowerhd
颜文字
Emoji
小恐龙
花!
上一篇
下一篇