首页 / TanStack 生态入门教程 / 列定义与行模型

TanStack 生态入门教程

列定义与行模型

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

TanStackTanStack 生态入门教程TanStack Table列定义ColumnDef行模型Row ModelcreateColumnHelper

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 自定义表头和单元格

headercell 都可以写成函数,拿到上下文信息后返回自定义内容。

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 能推导部分类型,但遇到 accessorFncell 函数时类型容易丢。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 用列助手定义列

列助手提供了 accessordisplay 两种方法:

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 的结构

渲染表格时你会频繁接触 RowCell 对象,搞清楚它们的结构很有必要。

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 渲染上下文

cellheader 函数拿到的上下文对象包含:

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):通过 accessorKeyaccessorFn 取值,headercell 可自定义渲染。
  • createColumnHelper:TypeScript 项目的列定义好帮手,类型推导完整,推荐使用。
  • display 列:不取数据的纯展示列(如操作列),用 columnHelper.display()
  • 行模型(Row Model):数据流水线,核心行模型是基础,排序/过滤/分页等各管一层。
  • Row / Cell 结构row.original 拿原始数据,getVisibleCells() 拿单元格,flexRender 渲染内容。

列定义和行模型是 TanStack Table 的骨架。下一章往骨架上加排序和过滤,让你的表格能交互起来。