mcpField
W
web-ui-tanstack-table

web-ui-tanstack-table

github.com/agents-inc开发2676 次安装2 天前更新
代码仓库

关于 web-ui-tanstack-table

面向使用 TanStack Table v8 构建类型安全无头表格的开发者,介绍行模型、表格状态以及客户端和服务端处理方式,不规定标记或样式。

主要功能

  1. 01类型化列定义
  2. 02排序、筛选与分页
  3. 03行选择与展开
  4. 04列显示与固定
  5. 05服务端数据处理模式
  6. 06稳定引用与行标识

适用场景

  1. 01构建自定义表格标记
  2. 02将表格状态连接到服务端查询
  3. 03添加选择和批量操作
  4. 04保持大型表格响应流畅

安装

复制下面的命令,在终端中运行。

$npx skills add https://github.com/agents-inc/skills --skill web-ui-tanstack-table

安装后在 Claude Code 中输入/web-ui-tanstack-table确认已加载。

包含的文件

除 SKILL.md 外,安装时一起下载的参考资料和脚本。

metadata.yaml
reference.md
examples/
column-pinning.md
column-resizing.md
column-visibility.md
core.md
expanding.md
filtering.md
pagination.md
selection.md
server-side.md
sorting.md
virtualization.md

详细介绍

TanStack Table 模式

快速指南: TanStack Table v8 只负责表格_逻辑_——它会计算行、单元格和表头组,但完全不负责渲染。你可以使用 createColumnHelper<T>() 描述数据,通过导入相应的行模型来启用各项功能,并根据实例返回的内容渲染任意标记。两个事实最容易引发问题:data 和 columns 必须保持引用稳定,否则表格会不断重新计算;而 manual* 标志用于告知它服务器已经完成了相应工作。

详细资源:


适用路径

排序、筛选和分页发生在哪里,是决定其他所有答案的分岔点。

  • 客户端持有所有行。 导入所需功能对应的行模型—— getSortedRowModel、getFilteredRowModel、getPaginationRowModel——由表格完成处理。 请参阅 examples/core.md 和各功能对应的文件。
  • 服务器已完成处理。 将 manualSorting、manualFiltering 和 manualPagination 设为 true,提供 rowCount,并且不导入这些行模型——设置了 manual* 标志后, 对应的行模型会被忽略,因此同时引入两者只是徒增负担,却会让人误以为该功能正常运行。 请参阅 examples/server-side.md。
  • 所有行都已存在,但渲染量太大。 保留客户端行模型,对输出进行窗口化,而不是分页。 请参阅 examples/virtualization.md。

<critical_requirements>

编写 TanStack Table 代码前

让 data 和 columns 保持引用稳定——使用 useMemo 或模块级常量。表格按引用标识进行比较,因此每次渲染都创建新数组,就意味着每次渲染都会产生一组新列;由此引发的更新循环表现为标签页卡死,而不是报错。

为列辅助函数指定类型:createColumnHelper<Row>()。 这样每个访问器键都会根据行类型进行检查,每个单元格的 getValue() 也会根据其读取的字段获得类型,这样重命名字段时就会触发编译错误,而不是生成一列 undefined。

为 accessorFn 指定明确的 id。 字符串 accessorKey 会提供自己的 ID;函数没有可用于推导 ID 的名称,因此没有 id 就无法通过该列进行排序、筛选或控制可见性。

在选择行或重新加载数据时设置 getRowId。 默认标识是数组索引,因此排序、筛选或重新获取数据后,选中状态会悄然转移到当前占据这些位置的行。

只导入用到的行模型。 每个行模型都有独立的入口点;未导入的行模型会被 tree-shaking 移除,这正是这些功能采用这种打包方式的原因。 通过 flexRender 渲染。header 和 cell 都可以是字符串或组件,flexRender 会处理这两种情况——直接读取 columnDef.header 看似可行,但遇到第一个提供函数的列时就会出问题。

</critical_requirements>


自动检测:@tanstack/react-table、useReactTable、createColumnHelper、columnHelper.accessor、columnHelper.display、columnHelper.group、getCoreRowModel、getSortedRowModel、getFilteredRowModel、getPaginationRowModel、getExpandedRowModel、flexRender、ColumnDef、SortingState、ColumnFiltersState、PaginationState、RowSelectionState、VisibilityState、ExpandedState、ColumnPinningState、manualPagination、manualSorting、accessorKey、accessorFn、getRowId

适用范围:

  • 定义列——访问器、计算值、显示列、分组表头
  • 表格状态:排序、筛选、分页、选择、展开、可见性、固定、尺寸
  • 选择客户端还是服务端操作,并接入相应模式
  • 读取实例以进行渲染:表头组、可见单元格、行模型
  • 保持大型表格响应流畅——记忆化、摇树优化、行输出窗口化

其他部分负责:

  • 每个元素和每种样式。此库返回数据结构;示例中的 <table> 只是渲染它们的一种方式,结果的外观不属于本技能的关注范围
  • 行的来源。服务端模式使用已分页的数组和总数,无论它们通过何种方式获取
  • 用作筛选和分页输入的控件——表格需要一个值和一个变更处理函数,至于生成它们的控件则无关紧要
  • 窗口化本身。表格会提供一个扁平的行数组;测量视口并渲染适合的部分属于另一项能力,而 examples/virtualization.md 仅展示两者之间的衔接方式

无头设计意味着此库不会强加任何需要你费力应对的观点。没有需要覆盖的主题,没有需要斗争的标记,也没有需要深入访问其内部的组件——实例负责回答问题,而你来决定绘制什么。

因此,**默认情况下什么功能都不会启用。**表格不支持排序并不是故障;它只是没有导入 getSortedRowModel。从组件库转来的人可能会因此困惑,因为在组件库中,功能是通过属性启用的。值得记住的是:当某项功能不起作用时,首先要检查是否提供了相应的行模型,其次要检查是否设置了 manual* 标记,告诉表格不要处理这项功能。

同样的设计也让此库与框架无关——核心部分是纯 TypeScript,React 适配器则很轻薄——这就是为什么其 API 表现为返回数据的函数,而不是负责执行操作的 Hook。


<decision_framework>

应导入哪些行模型

功能行模型备注
任何功能getCoreRowModel始终必需
排序getSortedRowModel设置 manualSorting 时省略
列筛选和全局筛选getFilteredRowModel设置 manualFiltering 时省略
分页getPaginationRowModel设置 manualPagination 时省略
可展开行、子行getExpandedRowModel
行分组getGroupedRowModel
筛选 UI 的不同取值getFacetedRowModel
列固定、列尺寸无两者都属于核心功能

如何声明列

Reading a field directly?          → columnHelper.accessor("email")
Reading a nested field?            → columnHelper.accessor("address.city")   (dot notation)
Deriving a value from the row?     → columnHelper.accessor(fn, { id: "…" })  (id is required)
No data at all — buttons, a checkbox?
                                   → columnHelper.display({ id: "…" })
Heading a set of other columns?    → columnHelper.group({ columns: [...] })

关键区别在于:访问器的返回值是排序和筛选的操作对象,因此它应保持为原始值。所有视觉内容都应放在 cell 中。

</decision_framework>


核心模式

模式 1:表格设置

实例由数据、列以及你选择启用的行模型构建。两项输入都会进行记忆化;getRowId 会用稳定标识替换基于索引的标识。

const columnHelper = createColumnHelper<User>();

const columns = useMemo(
  () => [
    columnHelper.accessor("firstName", { header: "First Name" }),
    columnHelper.accessor((row) => `${row.firstName} ${row.lastName}`, {
      id: "fullName", // required — a function has no key to derive one from
      header: "Full Name",
    }),
  ],
  [],
);

const table = useReactTable({
  data: useMemo(() => users, [users]),
  columns,
  getCoreRowModel: getCoreRowModel(),
  getRowId: (row) => row.id,
});

完整代码:examples/core.md

模式 2:排序

将状态提升到组件外部,使其可持久化;行模型负责排序。

const [sorting, setSorting] = useState<SortingState>([]);

const table = useReactTable({
  /* … */
  state: { sorting },
  onSortingChange: setSorting,
  getSortedRowModel: getSortedRowModel(),
});

columnHelper.accessor("createdAt", { sortingFn: "datetime" });

Date 对象需要使用 sortingFn: "datetime"——默认比较器会将它们视为字符串,因此会按星期名称排序。

完整代码:examples/sorting.md

模式 3:筛选

列筛选和全局筛选是相互独立的状态,各自有独立的处理器,但都由同一个行模型处理。

const [columnFilters, setColumnFilters] = useState<ColumnFiltersState>([]);
const [globalFilter, setGlobalFilter] = useState("");

const table = useReactTable({
  /* … */
  state: { columnFilters, globalFilter },
  onColumnFiltersChange: setColumnFilters,
  onGlobalFilterChange: setGlobalFilter,
  getFilteredRowModel: getFilteredRowModel(),
});

多个列筛选条件会取交集——一行必须满足所有条件。内置功能不支持并集;要实现并集,可以使用全局筛选或自定义 filterFn。

完整代码:examples/filtering.md

模式 4:分页
const [pagination, setPagination] = useState<PaginationState>({
  pageIndex: 0,
  pageSize: 10,
});

const table = useReactTable({
  /* … */
  state: { pagination },
  onPaginationChange: setPagination,
  getPaginationRowModel: getPaginationRowModel(),
});

pageIndex 从零开始计数。大多数 API 从一开始计数,因此转换应放在请求边界处,其他地方都不应转换。

完整代码:examples/pagination.md

模式 5:行选择
const [rowSelection, setRowSelection] = useState<RowSelectionState>({});

const table = useReactTable({
  /* … */
  state: { rowSelection },
  onRowSelectionChange: setRowSelection,
  enableRowSelection: true,
  getRowId: (row) => row.id, // the selection is keyed by this
});

选择状态是一个以行 ID 为键的映射,因此如果没有 getRowId,它就会以数组位置为键——表格排序后,每个选择项都会重新指向不同的记录。

完整代码:examples/selection.md

模式 6:服务端数据

manual* 标志会告知表格,传入的数组已经过排序、筛选和分页。表格随后只报告状态变化,不执行任何计算。

const table = useReactTable({
  data: apiData ?? [],
  columns,
  state: { pagination, sorting, columnFilters },
  onPaginationChange: setPagination,
  onSortingChange: setSorting,
  onColumnFiltersChange: setColumnFilters,
  getCoreRowModel: getCoreRowModel(),
  manualPagination: true,
  manualSorting: true,
  manualFiltering: true,
  rowCount: totalFromApi, // it cannot count rows it never received
});

完整代码:examples/server-side.md

模式 7:列可见性
const [columnVisibility, setColumnVisibility] = useState<VisibilityState>({
  email: false, // hidden until asked for
});

columnHelper.accessor("id", { enableHiding: false }); // never offered in the toggle UI

隐藏列会从 getVisibleCells() 中移除,但仍保留在表格中,因此它们的筛选和排序仍会生效。

完整代码:examples/column-visibility.md

模式 8:展开行
const [expanded, setExpanded] = useState<ExpandedState>({});

const table = useReactTable({
  /* … */
  state: { expanded },
  onExpandedChange: setExpanded,
  getExpandedRowModel: getExpandedRowModel(),
  getRowCanExpand: () => true, // or a predicate over row.original
});

展开行会交错插入行模型,而不是嵌套其中,因此要渲染详情面板,需要检查 row.getIsExpanded() 并自行输出一个额外元素。

完整代码:examples/expanding.md

模式 9:可复用的泛型表格

泛型让同一个组件可适用于所有行类型,同时不会丢失调用处的类型推断。

interface DataTableProps<TData, TValue> {
  columns: ColumnDef<TData, TValue>[];
  data: TData[];
}

export function DataTable<TData, TValue>({
  columns,
  data,
}: DataTableProps<TData, TValue>) {
  const table = useReactTable({
    data,
    columns,
    getCoreRowModel: getCoreRowModel(),
  });
  // render from table.getHeaderGroups() and table.getRowModel()
}

完整代码:examples/core.md

模式 10:列固定
const [columnPinning, setColumnPinning] = useState<ColumnPinningState>({
  left: ["id"],
  right: ["actions"],
});

固定列会重新排列列,并报告每列位于哪一侧。它不会设置任何定位样式——position: sticky、偏移量和不透明背景都需要你自行处理;背景用于避免滚动内容透显出来。

完整代码:examples/column-pinning.md

模式 11:列宽调整
const table = useReactTable({
  /* … */
  enableColumnResizing: true,
  columnResizeMode: "onChange", // or "onEnd"
});

"onChange" 会在拖动期间持续更新,并使所有单元格随之重新渲染,因此需要使用 CSS 变量技术和经过记忆化的表格主体来维持帧率。"onEnd" 会在松开时提交,不需要这两项技术。

完整代码:examples/column-resizing.md


<red_flags>

危险信号

运行时会出错:

  • columns 或 data 在行内构建——每次渲染都会生成新的数组标识,导致表格陷入更新循环,直到标签页停止响应
  • accessorFn 未设置 id——列没有标识符,构建实例时会抛出错误
  • 使用客户端行模型,同时设置对应的 manual* 标志——标志优先级更高,因此行模型不起作用,但看起来像是功能已接入
  • 设置 manualPagination,却没有 rowCount 或 pageCount——表格无法从单页推导出总页数,因此翻页会停在第 1 页
  • 选择功能未设置 getRowId——以数组索引作为键,因此排序或重新获取数据后,选中状态会对应到不同记录
  • 在访问器中返回 JSX——排序和筛选会比较 React 元素而非值,导致列的排序毫无意义
  • 直接读取 columnDef.header 或 columnDef.cell,而不是通过 flexRender——字符串表头可正常工作,其他列则都会渲染原始函数

容易令人意外的行为:

  • pageIndex 从 0 开始,而大多数 API 从 1 开始
  • autoResetPageIndex 默认为 true,因此任何数据变更都会跳回第 1 页——在服务端模式下通常不正确,因为新数据到达_本身_就是页面变更
  • 列筛选条件按 AND 组合;没有内置的 OR
  • 列固定和列宽调整不需要行模型,这一点与其他所有功能都不同
  • 固定操作发生在列排序和分组之前,因此两者有冲突时固定操作优先
  • 调整大小的拖动手柄需要同时使用 getResizeHandler() 提供的 onMouseDown 和 onTouchStart,否则在触屏设备上不起作用
  • 隐藏的列仍会参与筛选和排序;可见性只影响 getVisibleCells(),不会影响上游处理

评论

支持 Markdown