Table 入门:headless 表格理念
本教程共 38 篇 · 第 26 篇 · 更新于 2026-07-27 · 约 10 分钟阅读
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 像个毛坯房加一套施工图纸,格局你自己定,但啥都得自己动手。
Noteheadless 不是 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,值得单独说清楚。
列定义里,header 和 cell 都有两种写法:
// 写法一:字符串/值
{ 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 Table | ag-Grid |
|---|---|---|
| 设计理念 | headless,只管逻辑 | 组件库,逻辑+UI 都包 |
| 样式控制 | 100% 自定义 | 社区版有限,企业版可定制 |
| 框架支持 | React/Vue/Solid/Svelte/Angular | React/Angular/Vue/原生 JS |
| 开箱即用 | 需自己写渲染代码 | 传配置即用 |
| 体积 | 小,按需引入 | 较大(社区版也有几百 KB) |
| 高级功能 | 排序/过滤/分页/分组/固定/选择都有 | 功能更全(树形、Excel 导出、透视表等) |
| 企业版 | 无,全免费 MIT | 企业版收费(树形、导出等高级功能) |
| TypeScript | 类型安全,泛型推导好 | 有类型但推导不如 TanStack |
怎么选?看你的需求:
- 要快速出活、功能全(Excel 导出、透视表、树形数据),用 ag-Grid。
- 要完全控制样式、轻量、免费、类型安全,用 TanStack Table。
- 要自己造表格组件库给别人用,TanStack Table 是最佳底座。
Warningag-Grid 的很多高级功能(行分组、Excel 导出、列菜单)是企业版才有的,收费不便宜。如果你的需求只是排序、过滤、分页、固定列这些,TanStack Table 全免费就够。
26.9 常见坑
坑一:忘了传 getCoreRowModel。 不传的话 table.getRowModel().rows 是空的,表格啥也不显示。这是最基础的行模型,必须有。
坑二:用了 v7 的 API。 网上老教程用的是 useTable、columns 里用 accessor 而不是 accessorKey。v8 全改了,Hook 叫 useReactTable,取值用 accessorKey 或 accessorFn。别抄老代码。
坑三:直接渲染 columnDef.header 不用 flexRender。 如果 header 写的是函数,不经过 flexRender 就会打印出函数对象而不是内容。渲染表头和单元格一律用 flexRender。
坑四:以为有默认样式。 TanStack Table 不带任何 CSS,渲染出来是裸的 <table>。不自己加样式,就是一个没边框的原始表格。我第一次用以为库坏了。
坑五:在列定义里用了未定义的数据字段。 accessorKey: 'emial'(拼错了),表格不会报错,但对应单元格会显示空值。字段名一定对齐数据结构。
26.10 小结
这一章你认识了 TanStack Table:
- headless 设计:只管表格逻辑(排序、过滤、分页等),UI 完全你自己写。
- 安装:
npm install @tanstack/react-table,认准 v8。 - 核心 Hook:
useReactTable接收数据和列定义,返回表格实例。 - 基础渲染:表头用
getHeaderGroups(),表体用getRowModel().rows,内容渲染一律用flexRender。 - 和 ag-Grid 的区别:headless vs 组件库,自由度 vs 开箱即用。
下一章深入讲列定义(ColumnDef)和行模型(Row Model),这是理解 TanStack Table 数据流的关键。搞懂了这两样,后面的排序、过滤、分页都是顺水推舟。