列定义与行模型
本教程共 38 篇 · 第 27 篇 · 更新于 2026-07-27 · 约 11 分钟阅读
27. 列定义与行模型
本节目标:掌握列定义(ColumnDef)的完整写法,学会用
createColumnHelper写类型安全的列、自定义表头和单元格、理解行模型(Row Model)的数据流转过程。学完你能灵活控制每列展示什么内容,并理解表格内部的数据管线。
27.1 列定义是表格的灵魂
上一章我们用最简方式写了列定义:
const columns = [
{ header: '姓名', accessorKey: 'name' },
{ header: '年龄', accessorKey: 'age' },
]
这能用,但只是皮毛。列定义(ColumnDef)是 TanStack Table 里信息量最大的概念—表头怎么显示、数据从哪取、单元格渲染什么、列能不能排序、列宽多少、列怎么分组,全写在列定义里。
搞懂列定义,TanStack Table 就会了一半。
27.2 列定义的两种取值方式
每列要展示的数据,通过「取值器」从行数据里提取。有两种方式。
27.2.1 accessorKey:按字段名取值
最常用的方式,直接写字段名:
const columns = [
{
header: '姓名',
accessorKey: 'name', // 等价于 row.name
},
]
accessorKey 就是从数据对象里取同名属性的值。数据是 { name: '张三', age: 28 },accessorKey: 'name' 取到的就是 '张三'。
27.2.2 accessorFn:用函数取值
字段名取不了的值,用函数算。比如取全名(姓 + 名拼接)、取嵌套字段、做格式化:
const columns = [
{
header: '全名',
accessorFn: (row) => `${row.firstName} ${row.lastName}`,
},
{
header: '城市',
accessorFn: (row) => row.address.city, // 嵌套字段
},
]
accessorFn 接收行数据,返回要显示的值。灵活度最高,但有个注意点:用了 accessorFn 的列,默认没有 accessorKey,在排序和过滤时可能需要额外处理(后面章节讲)。
Tip嵌套字段也能用
accessorKey写,比如accessorKey: 'address.city'。但更推荐用accessorFn,类型推导更准确。
27.3 自定义表头和单元格
header 和 cell 都可以写成函数,拿到上下文信息后返回自定义内容。
27.3.1 自定义表头
const columns = [
{
header: '姓名',
accessorKey: 'name',
},
{
header: () => <span className="font-bold">年龄</span>,
accessorKey: 'age',
},
]
字符串和函数都行,渲染时记得用 flexRender 统一处理。
27.3.2 自定义单元格
cell 写成函数时,能拿到一个上下文对象,里面有行数据、列信息、表格实例等:
const columns = [
{
header: '姓名',
accessorKey: 'name',
cell: ({ row }) => <strong>{row.original.name}</strong>,
},
{
header: '年龄',
accessorKey: 'age',
cell: ({ getValue }) => {
const age = getValue() as number
return <span className={age > 30 ? 'text-red-500' : 'text-green-500'}>{age} 岁</span>
},
},
{
header: '操作',
id: 'actions', // 没有 accessorKey 时必须给 id
cell: ({ row }) => (
<button onClick={() => alert(row.original.id)}>编辑</button>
),
},
]
几个关键点:
row.original是这行的原始数据对象,你想取啥都行。getValue()返回当前列通过 accessor 取到的值。- 不取数据、只做展示的列(比如「操作」列),没有
accessorKey,必须手动给个id。
Note不写
cell时,默认行为是直接显示getValue()的值。写了cell就完全由你控制渲染内容。
27.4 createColumnHelper:类型安全的好帮手
直接写对象数组定义列,TypeScript 能推导部分类型,但遇到 accessorFn 和 cell 函数时类型容易丢。createColumnHelper 专门解决这个问题。
27.4.1 创建列助手
先定义数据类型,再用类型创建列助手:
import { createColumnHelper } from '@tanstack/react-table'
// 定义行数据类型
type Person = {
id: number
firstName: string
lastName: string
age: number
address: {
city: string
}
}
const columnHelper = createColumnHelper<Person>()
27.4.2 用列助手定义列
列助手提供了 accessor 和 display 两种方法:
const columns = [
// accessor 列:有数据取值的列
columnHelper.accessor('firstName', {
header: '名',
cell: ({ row }) => <span>{row.original.firstName}</span>,
}),
columnHelper.accessor('age', {
header: '年龄',
cell: (info) => <span>{info.getValue()} 岁</span>,
}),
// accessorFn 也能用
columnHelper.accessor((row) => row.address.city, {
id: 'city',
header: '城市',
}),
// display 列:不取数据,纯展示
columnHelper.display({
id: 'actions',
header: '操作',
cell: ({ row }) => <button onClick={() => edit(row.original.id)}>编辑</button>,
}),
]
用列助手的好处:
accessor的第一个参数有类型提示,写错字段名直接报红。cell函数里的info.getValue()类型推导准确,不用手动as。display明确标识「这列不取数据」,语义更清晰。
Tip项目用 TypeScript 的话,强烈建议用
createColumnHelper。类型提示能帮你省不少调试时间。
27.5 列的其他常用属性
除了取值和渲染,列定义还有些常用属性:
const columns = [
columnHelper.accessor('name', {
header: '姓名',
id: 'name', // 列唯一标识,不写则用 accessorKey
enableSorting: false, // 禁用排序(第 28 章讲)
enableColumnFilter: false, // 禁用列过滤
size: 200, // 列宽建议值(像素)
minSize: 100, // 最小列宽
maxSize: 400, // 最大列宽
}),
]
id:列的唯一标识。不写时用accessorKey当 id。用了accessorFn时必须手动写id。enableSorting/enableColumnFilter:控制这列能不能排序、过滤,默认true。size/minSize/maxSize:列宽相关,第 30 章讲列固定和列宽调整时详细说。
27.6 行模型:数据怎么变成行
理解了列定义,接下来看行模型(Row Model)。这是 TanStack Table 的数据管线核心。
打个比方:原材料(你的 data 数组)进了工厂,经过一道道工序(排序、过滤、分页),最后出来的成品(渲染用的行)就是行模型的结果。
27.6.1 核心行模型
最基础的是核心行模型(Core Row Model),它把原始数据转成表格能理解的行结构:
import { useReactTable, getCoreRowModel } from '@tanstack/react-table'
const table = useReactTable({
data,
columns,
getCoreRowModel: getCoreRowModel(),
})
核心行模型做的事很简单:遍历 data 数组,每条数据创建一个 Row 对象。Row 里包含原始数据、展开状态、选中状态等。核心行模型不做任何过滤、排序、分页,数据原封不动转成行。
27.6.2 行模型的层级
TanStack Table 的行模型是流水线设计,每一层在上层基础上加工:
原始数据
↓ getCoreRowModel → 核心行(所有行)
↓ getSortedRowModel → 排序后的行
↓ getFilteredRowModel → 过滤后的行
↓ getGroupedRowModel → 分组后的行
↓ getExpandedRowModel → 展开后的行
↓ getPaginatedRowModel → 分页后的行
你用哪些功能就接哪些行模型。只用核心功能就传 getCoreRowModel;要排序就加 getSortedRowModel;要分页就加 getPaginatedRowModel。各层按顺序处理,上一层的输出是下一层的输入。
const table = useReactTable({
data,
columns,
getCoreRowModel: getCoreRowModel(),
getSortedRowModel: getSortedRowModel(),
getFilteredRowModel: getFilteredRowModel(),
getPaginationRowModel: getPaginationRowModel(),
})
Note行模型的执行顺序是固定的,不受你传参顺序影响。TanStack Table 内部按 核心 → 排序 → 过滤 → 分组 → 展开 → 分页 的顺序处理。你只需要把要用到的行模型都传进去。
27.7 Row 和 Cell 的结构
渲染表格时你会频繁接触 Row 和 Cell 对象,搞清楚它们的结构很有必要。
27.7.1 Row 对象
table.getRowModel().rows.map((row) => {
// row.id - 行的唯一标识,通常是索引或自定义 key
// row.index - 行在当前行模型中的索引
// row.original - 这行的原始数据对象
// row.depth - 行的嵌套深度(分组时用)
// row.getVisibleCells() - 返回这行所有可见单元格
// row.getIsSelected() - 这行是否被选中
// row.getCanExpand() - 这行能否展开
})
最常用的是 row.original(拿原始数据)和 row.getVisibleCells()(拿单元格渲染)。
27.7.2 Cell 对象
row.getVisibleCells().map((cell) => {
// cell.id - 单元格唯一标识
// cell.row - 所属行对象
// cell.column - 所属列对象
// cell.getContext() - 渲染上下文(传给 flexRender)
})
渲染单元格内容时,flexRender(cell.column.columnDef.cell, cell.getContext()) 里的 cell.getContext() 就是把行、列、表格实例等信息打包传给 cell 函数。
27.7.3 渲染上下文
cell 和 header 函数拿到的上下文对象包含:
cell: ({ table, column, row, getValue, renderValue }) => {
// table - 表格实例
// column - 当前列对象
// row - 当前行对象
// getValue() - 当前列的取值
// renderValue() - 渲染后的值(处理了缺省情况)
}
有了这些,你能在单元格里做几乎任何事情:跨行取数据、调表格方法、根据列配置渲染不同内容。
27.8 一个完整的列定义示例
把前面学的组合起来,写一个稍完整点的表格:
import { useReactTable, getCoreRowModel, createColumnHelper, flexRender } from '@tanstack/react-table'
type Person = {
id: number
firstName: string
lastName: string
age: number
status: 'active' | 'inactive'
}
const data: Person[] = [
{ id: 1, firstName: '张', lastName: '三', age: 28, status: 'active' },
{ id: 2, firstName: '李', lastName: '四', age: 34, status: 'inactive' },
{ id: 3, firstName: '王', lastName: '五', age: 22, status: 'active' },
]
const columnHelper = createColumnHelper<Person>()
const columns = [
columnHelper.accessor('id', {
header: 'ID',
size: 60,
}),
columnHelper.accessor((row) => `${row.firstName}${row.lastName}`, {
id: 'fullName',
header: '全名',
cell: ({ row }) => (
<span className="font-medium">{row.original.firstName}{row.original.lastName}</span>
),
}),
columnHelper.accessor('age', {
header: '年龄',
cell: ({ getValue }) => <span>{getValue()} 岁</span>,
}),
columnHelper.accessor('status', {
header: '状态',
cell: ({ getValue }) => {
const status = getValue()
return (
<span className={status === 'active' ? 'text-green-600' : 'text-gray-400'}>
{status === 'active' ? '在线' : '离线'}
</span>
)
},
}),
columnHelper.display({
id: 'actions',
header: '操作',
cell: ({ row }) => (
<button
onClick={() => console.log('编辑', row.original.id)}
className="text-blue-500 hover:underline"
>
编辑
</button>
),
}),
]
function PersonTable() {
const table = useReactTable({
data,
columns,
getCoreRowModel: getCoreRowModel(),
})
return (
<table className="border-collapse">
<thead>
{table.getHeaderGroups().map((headerGroup) => (
<tr key={headerGroup.id}>
{headerGroup.headers.map((header) => (
<th key={header.id} className="border border-gray-300 px-4 py-2 bg-gray-50">
{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} className="border border-gray-300 px-4 py-2">
{flexRender(cell.column.columnDef.cell, cell.getContext())}
</td>
))}
</tr>
))}
</tbody>
</table>
)
}
这个表格有自定义全名列(accessorFn)、格式化年龄和状态(cell 函数)、操作列(display),基本覆盖了列定义的常见用法。
27.9 常见坑
坑一:用了 accessorFn 但忘了写 id。 没有 accessorKey 的列,必须手动给 id,否则表格报错或者列之间互相覆盖。
坑二:cell 函数里取值用错方式。 cell: ({ row }) => row.name 只有在数据有 name 字段时才行。更通用的写法是 cell: ({ getValue }) => getValue(),它走 accessor 取值,不管你用 accessorKey 还是 accessorFn 都对。
坑三:不用 createColumnHelper 导致类型丢失。 直接写对象数组,cell 函数里的 getValue() 返回 unknown,得手动断言。用列助手一切自动推导。
坑四:以为行模型只有一层。 只传了 getCoreRowModel 就指望排序生效—不会的。排序要传 getSortedRowModel,分页要传 getPaginationRowModel。各功能各管各的行模型。
坑五:列定义每次渲染都重新创建。 如果 columns 数组写在组件内部、每次渲染都新建,可能导致不必要的重渲染。把列定义提到组件外面,或者用 useMemo 包一层。
27.10 小结
这一章深入了列定义和行模型:
- 列定义(ColumnDef):通过
accessorKey或accessorFn取值,header和cell可自定义渲染。 - createColumnHelper:TypeScript 项目的列定义好帮手,类型推导完整,推荐使用。
- display 列:不取数据的纯展示列(如操作列),用
columnHelper.display()。 - 行模型(Row Model):数据流水线,核心行模型是基础,排序/过滤/分页等各管一层。
- Row / Cell 结构:
row.original拿原始数据,getVisibleCells()拿单元格,flexRender渲染内容。
列定义和行模型是 TanStack Table 的骨架。下一章往骨架上加排序和过滤,让你的表格能交互起来。