首页 / TanStack 生态入门教程 / Table 入门:headless 表格理念

TanStack 生态入门教程

Table 入门:headless 表格理念

本教程共 38 篇 · 第 26 篇 · 更新于 2026-07-27 · 约 10 分钟阅读

TanStackTanStack 生态入门教程TanStack TableheadlessuseReactTable表格ag-Grid安装

26. Table 入门:headless 表格理念

本节目标:理解 TanStack Table 为什么叫 headless(无头),学会安装和用 useReactTable 搭一个最小表格,搞清楚它和 ag-Grid 这类表格组件库的本质区别。学完你能从零渲染出一个结构完整的数据表格。

26.1 什么是 headless 表格

前面讲 Query 管「数据从哪来」,Router 管「页面切到哪」,这一章开始讲表格(Table)

但 TanStack Table 跟你用过的表格库不太一样。ag-Grid、Ant Design Table 这些库,你传个数据进去,它帮你把表格画好了—表头什么样、边框什么颜色、排序按钮在哪,都定死了。方便是方便,但想改个布局、换个样式,就得跟它的 CSS 斗智斗勇。

TanStack Table 走了另一条路:只管逻辑,不管外观。它帮你算哪些行该显示、怎么排序、怎么分页,但画成什么样完全你说了算。这种只给逻辑不给 UI 的设计,就叫 headless(无头)

打个比方:ag-Grid 像个精装修房子,拎包入住但改不了格局;TanStack Table 像个毛坯房加一套施工图纸,格局你自己定,但啥都得自己动手。

Note

headless 不是 TanStack 独有的概念。整个 TanStack 生态的表格、表单、虚拟列表都是 headless 设计。第二章讲过这个理念,这里结合表格再感受一次。

26.2 headless 的好处和代价

先说好处:

  • 样式 100% 你做主:用 Tailwind、用 CSS Modules、用 styled-components,都行。不用 !important 去覆盖库的样式。
  • 框架无关:同一套表格逻辑,React、Vue、Solid、Svelte 都能用。核心包不依赖任何框架。
  • 体积小:不带 UI 代码,按需引入功能模块,打包体积可控。
  • 类型安全:用 TypeScript 写列定义,列的类型、数据的类型都能推导出来。

再说代价:

  • 手活多:表头、表体、单元格都得你自己写 JSX 来渲染。
  • 学习曲线陡:得理解它的列定义、行模型等概念,不像 ag-Grid 传个 columns 配置就完事。
Tip

如果你项目急、要的是开箱即用的表格,选 ag-Grid 或 Ant Design Table。如果你要高度定制、或者要做一套自己的表格组件库给别人用,TanStack Table 是最好的底层。

26.3 安装

26.3.1 装包

React 项目里装适配器包就行:

npm install @tanstack/react-table

核心逻辑在 @tanstack/table-core 里,React 适配器包已经帮你依赖好了,不用单独装。

Note

本教程基于 Table v8(当前稳定版 8.21.x)。v8 是一次大改版:包名从 react-table 改成 @tanstack/react-table,Hook 从 useTable 改成 useReactTable,列定义方式也变了。网上有些老文章还在用 v7 的 API,别照抄。

26.3.2 检查版本

装完看一眼 package.json

{
  "dependencies": {
    "@tanstack/react-table": "^8.21.3"
  }
}

版本号 8.x 就对了。如果看到 7.x,那是老版本,API 完全不一样。

26.4 useReactTable:表格的大脑

useReactTable 是 React 适配器的核心 Hook,所有表格逻辑从这里开始。

先看一个最小表格长啥样,再拆解每一步:

import { useReactTable, getCoreRowModel, flexRender } from '@tanstack/react-table'

// 假数据
const data = [
  { id: 1, name: '张三', age: 28 },
  { id: 2, name: '李四', age: 34 },
  { id: 3, name: '王五', age: 22 },
]

// 列定义:告诉表格每一列展示什么
const columns = [
  { header: '姓名', accessorKey: 'name' },
  { header: '年龄', accessorKey: 'age' },
]

function BasicTable() {
  const table = useReactTable({
    data,
    columns,
    getCoreRowModel: getCoreRowModel(),
  })

  return (
    <table>
      <thead>
        {table.getHeaderGroups().map((headerGroup) => (
          <tr key={headerGroup.id}>
            {headerGroup.headers.map((header) => (
              <th key={header.id}>
                {flexRender(header.column.columnDef.header, header.getContext())}
              </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>
  )
}

这段代码看着不少,但结构很清晰。下面一步步拆。

26.5 拆解基础表格结构

26.5.1 数据和列定义

表格要两个输入:数据列定义(ColumnDef)

数据就是一个对象数组,每条数据是一行:

const data = [
  { id: 1, name: '张三', age: 28 },
  { id: 2, name: '李四', age: 34 },
]

列定义告诉表格每列的表头叫什么、数据从哪个字段取:

const columns = [
  { header: '姓名', accessorKey: 'name' }, // accessorKey 对应数据字段名
  { header: '年龄', accessorKey: 'age' },
]

accessorKey 是「取值键」,表格用它从每行数据里取对应字段的值来显示。header 是表头文字。

Note

列定义的完整写法和技巧下一章专门讲,这里先用最简形式感受整体流程。

26.5.2 创建表格实例

把数据和列定义交给 useReactTable,拿到一个表格实例(Table Instance)

const table = useReactTable({
  data,
  columns,
  getCoreRowModel: getCoreRowModel(),
})

getCoreRowModel核心行模型(Core Row Model),负责把原始数据转换成表格能渲染的行结构。没有它,表格不知道怎么把数据变成行。这是最基础的行模型,排序、过滤、分页等高级行模型后面章节再加。

26.5.3 渲染表头

表格实例上有 getHeaderGroups() 方法,返回表头分组。通常只有一层,但做了列分组后会有多层:

<thead>
  {table.getHeaderGroups().map((headerGroup) => (
    <tr key={headerGroup.id}>
      {headerGroup.headers.map((header) => (
        <th key={header.id}>
          {flexRender(header.column.columnDef.header, header.getContext())}
        </th>
      ))}
    </tr>
  ))}
</thead>

注意渲染表头内容用的是 flexRender,不是直接写 header.column.columnDef.header。因为 header 可以是个字符串,也可以是个返回 JSX 的函数。flexRender 帮你统一处理这两种情况。

26.5.4 渲染表体

表体逻辑类似,先拿行模型,再遍历每行的单元格:

<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.getRowModel() 返回当前行模型,.rows 是所有行。每行调 getVisibleCells() 拿可见单元格。

单元格内容用 cell.column.columnDef.cell 渲染。你没写 cell 的话,默认行为是显示 accessorKey 对应的数据值。下一章会讲怎么自定义单元格内容。

Tip

flexRender 这个名字容易让人懵。它的作用就是:如果传入的是函数就调用它并传入上下文,如果是字符串/JSX 就直接返回。相当于一个智能的「渲染分发器」。

26.6 flexRender 是什么

上面两处渲染都用了 flexRender,值得单独说清楚。

列定义里,headercell 都有两种写法:

// 写法一:字符串/值
{ header: '姓名', accessorKey: 'name' }

// 写法二:函数(能拿到上下文信息)
{
  header: () => <span>姓名</span>,
  cell: ({ row }) => <strong>{row.original.name}</strong>,
}

如果你直接写 {header.column.columnDef.header},函数那种写法就不会被执行,会原样把函数打印出来。

flexRender 解决这个问题:

// 不管是字符串还是函数,都能正确渲染
flexRender(header.column.columnDef.header, header.getContext())
flexRender(cell.column.columnDef.cell, cell.getContext())

它内部判断:是函数就调,不是就直接返回。你只要记住渲染表头和单元格内容时,一律用 flexRender 就行。

26.7 加点样式

上面渲染出来是个裸表格,没边框没间距。因为 headless 不管样式,你得自己加。用 Tailwind 试试:

function StyledTable() {
  const table = useReactTable({
    data,
    columns,
    getCoreRowModel: getCoreRowModel(),
  })

  return (
    <table className="border-collapse border border-gray-300">
      <thead>
        {table.getHeaderGroups().map((headerGroup) => (
          <tr key={headerGroup.id}>
            {headerGroup.headers.map((header) => (
              <th
                key={header.id}
                className="border border-gray-300 bg-gray-100 px-4 py-2 text-left"
              >
                {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.getVisibleCells().map((cell) => (
              <td key={cell.id} className="border border-gray-300 px-4 py-2">
                {flexRender(cell.column.columnDef.cell, cell.getContext())}
              </td>
            ))}
          </tr>
        ))}
      </tbody>
    </table>
  )
}

样式全在 className 里,想怎么改怎么改。这就是 headless 的自由。

26.8 和 ag-Grid 对比

很多团队在 ag-Grid 和 TanStack Table 之间纠结,这里给个对比帮你判断。

对比项TanStack Tableag-Grid
设计理念headless,只管逻辑组件库,逻辑+UI 都包
样式控制100% 自定义社区版有限,企业版可定制
框架支持React/Vue/Solid/Svelte/AngularReact/Angular/Vue/原生 JS
开箱即用需自己写渲染代码传配置即用
体积小,按需引入较大(社区版也有几百 KB)
高级功能排序/过滤/分页/分组/固定/选择都有功能更全(树形、Excel 导出、透视表等)
企业版无,全免费 MIT企业版收费(树形、导出等高级功能)
TypeScript类型安全,泛型推导好有类型但推导不如 TanStack

怎么选?看你的需求:

  • 快速出活、功能全(Excel 导出、透视表、树形数据),用 ag-Grid。
  • 完全控制样式、轻量、免费、类型安全,用 TanStack Table。
  • 自己造表格组件库给别人用,TanStack Table 是最佳底座。
Warning

ag-Grid 的很多高级功能(行分组、Excel 导出、列菜单)是企业版才有的,收费不便宜。如果你的需求只是排序、过滤、分页、固定列这些,TanStack Table 全免费就够。

26.9 常见坑

坑一:忘了传 getCoreRowModel 不传的话 table.getRowModel().rows 是空的,表格啥也不显示。这是最基础的行模型,必须有。

坑二:用了 v7 的 API。 网上老教程用的是 useTablecolumns 里用 accessor 而不是 accessorKey。v8 全改了,Hook 叫 useReactTable,取值用 accessorKeyaccessorFn。别抄老代码。

坑三:直接渲染 columnDef.header 不用 flexRender 如果 header 写的是函数,不经过 flexRender 就会打印出函数对象而不是内容。渲染表头和单元格一律用 flexRender

坑四:以为有默认样式。 TanStack Table 不带任何 CSS,渲染出来是裸的 <table>。不自己加样式,就是一个没边框的原始表格。我第一次用以为库坏了。

坑五:在列定义里用了未定义的数据字段。 accessorKey: 'emial'(拼错了),表格不会报错,但对应单元格会显示空值。字段名一定对齐数据结构。

26.10 小结

这一章你认识了 TanStack Table:

  • headless 设计:只管表格逻辑(排序、过滤、分页等),UI 完全你自己写。
  • 安装npm install @tanstack/react-table,认准 v8。
  • 核心 HookuseReactTable 接收数据和列定义,返回表格实例。
  • 基础渲染:表头用 getHeaderGroups(),表体用 getRowModel().rows,内容渲染一律用 flexRender
  • 和 ag-Grid 的区别:headless vs 组件库,自由度 vs 开箱即用。

下一章深入讲列定义(ColumnDef)和行模型(Row Model),这是理解 TanStack Table 数据流的关键。搞懂了这两样,后面的排序、过滤、分页都是顺水推舟。