---
id: headless-issue-table
track: learn
product: table
locale: zh-CN
order: 18
revision: 1
sourceCheckedOn: "2026-08-03"
versionRange: "Table ^8"
verifiedAgainst: "@tanstack/react-table@8.21.3"
contentModel: 2
prerequisites:
  - "会渲染 React 列表与 table 元素"
  - "理解 TypeScript 对象类型"
sourceRefs:
  - "https://tanstack.com/table/latest/docs/introduction"
  - "https://tanstack.com/table/latest/docs/guide/column-defs"
  - "https://tanstack.com/table/latest/docs/guide/rows"
---

# 把 Issue 数据组织成无头表格

> 用类型化列定义、核心行模型和 flexRender 连接数据与语义化 table 标记，同时保留完整的样式控制权。

**Outcome:** 能构建类型安全、行标识稳定且不依赖预制 UI 的基础数据表。

## Table 管逻辑，不替你设计界面

TanStack Table 是无头库：它产生列、表头、行、单元格和交互状态模型，但最终 HTML、组件、键盘行为和 CSS 仍由应用负责。先用原生 table 建立正确语义，再把同一实例接入你的设计系统；不要寻找一个不存在的 Table 组件。

## 列定义同时描述数据与渲染

createColumnHelper 把列约束到 Issue 类型。accessor 列应返回可排序、筛选或分组的原始值；只负责按钮或链接的 display 列不应伪造 accessor。函数 accessor 必须提供稳定 id 或字符串 header。

File: `src/features/issues/IssueTable.tsx`

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

type Issue = { id: string; title: string; status: 'open' | 'closed' }
const column = createColumnHelper<Issue>()
const columns = [
  column.accessor('title', { header: 'Title' }),
  column.accessor('status', { header: 'Status' }),
  column.display({ id: 'actions', cell: ({ row }) => <a href={`/issues/${row.original.id}`}>View</a> }),
]

export function IssueTable({ issues }: { issues: Issue[] }) {
  const table = useReactTable({
    data: issues, columns, getRowId: (issue) => issue.id,
    getCoreRowModel: getCoreRowModel(),
  })
  return <table><thead>{table.getHeaderGroups().map((group) => (
    <tr key={group.id}>{group.headers.map((header) => <th key={header.id}>
      {header.isPlaceholder ? null : 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>
}
```

## 行模型是一条显式处理管线

getCoreRowModel 把原始 data 转成基础行。之后只有在需要排序、筛选或分页时才加入对应行模型。用 getRowId 返回业务 id，避免列表重排后选择状态、展开状态或编辑状态跟着数组位置漂移。空数据应传稳定空数组并渲染明确空状态。

> **Note:** 对于服务端数据，Table 负责视图模型；请求、缓存和新鲜度仍属于 Query 或你的数据层。

## 实践与验证

### 完成检查点

能从同一份 Issue 类型推导列值、行对象和渲染上下文，并说明 Table 为什么不输出固定组件。

### 动手练习

为 Issue 列表增加 title、status 和 priority 三列，再添加一个没有 accessor 的“查看详情”展示列。

### 验证方法

交换数据顺序后 row.id 仍应等于 Issue id；空数组应正常渲染表头和空状态，类型检查拒绝不存在的 accessor key。

### 常见错误

- 把 accessor 返回对象，随后期待默认排序可以理解业务字段。
- 使用数组索引作为服务端数据的稳定行标识。

## Official sources

- https://tanstack.com/table/latest/docs/introduction
- https://tanstack.com/table/latest/docs/guide/column-defs
- https://tanstack.com/table/latest/docs/guide/rows
