Refine 与 TanStack Table 集成实战:用 useTable 实现列级筛选(Column Filtering)
【免费下载链接】refineA React Framework for building internal tools, admin panels, dashboards & B2B apps with unmatched flexibility.项目地址: https://gitcode.com/GitHub_Trending/re/refine
本文围绕 Refine 仓库中 @refinedev/react-table 的列级筛选 Live Preview 示例(
documentation/docs/packages/tanstack-table/examples/_partial-filtering-live-preview.md)展开,讲解如何用useTable把 TanStack Table 的列筛选状态与 Refine 的服务端过滤(CrudFilters)打通,并通过meta.filterOperator精确控制每个字段的过滤算子。读完本文你将掌握:如何在 headless 表格中渲染可筛选表头、如何将列筛选翻译成 Refine 数据提供者能识别的过滤条件,以及条件过滤(and/or)和filterKey的使用方式。
一、背景:无头表格适配器如何工作
Refine 通过 TanStack Table 集成介绍 提供了@refinedev/react-table这个适配器包,它让你在使用 TanStack Table 的同时,自动继承 RefineuseTable的分页、排序、过滤等全部能力。底层数据获取走的是useList(即 data provider 的getList),同时因为它被设计为headless,UI 渲染完全由你控制。
安装方式:
npm install @refinedev/react-table从源码看,这个适配器的入口定义在 packages/react-table/src/useTable/index.ts:
- 内部先调用核心包的
useTable(useTableCore),拿到filters、setFilters、sorters、currentPage等 Refine 状态; - 再调用
useReactTable创建 TanStack Table 实例; - 通过一组工具函数在「TanStack 列筛选状态(
ColumnFiltersState)」与「Refine 过滤状态(CrudFilters)」之间做双向翻译。
其中的关键开关:
manualPagination: true:分页始终由服务端(Refine 数据提供者)控制;manualSorting: isServerSideFilteringEnabled:只有当sorters.mode不是"off"时才开启服务端排序;manualFiltering: isServerSideFilteringEnabled:只有当filters.mode不是"off"时才开启服务端过滤(此时getFilteredRowModel()不会被挂载)。
也就是说,默认情况下每次筛选变化都会触发一次新的数据请求。
二、核心示例:可筛选的 PostList 表格
下面是被嵌入useTable文档 Filtering 小节的可运行示例,去掉 Live Preview 包装后的核心代码(完整源码见 examples/_partial-filtering-live-preview.md):
import React from "react"; import { useTable } from "@refinedev/react-table"; import { ColumnDef, flexRender } from "@tanstack/react-table"; interface IPost { id: number; title: string; content: string; status: "published" | "draft" | "rejected"; } const PostList: React.FC = () => { const columns = React.useMemo<ColumnDef<IPost>[]>( () => [ { id: "id", header: "ID", accessorKey: "id", enableColumnFilter: false, // 该列禁用列筛选 }, { id: "title", header: "Title", accessorKey: "title", meta: { filterOperator: "contains", // 使用模糊包含匹配 }, }, { id: "status", header: "Status", accessorKey: "status", meta: { filterOperator: "contains", }, }, { id: "createdAt", header: "CreatedAt", accessorKey: "createdAt", meta: { filterOperator: "gte", // 使用大于等于 }, }, ], [], ); const { reactTable: { getHeaderGroups, getRowModel }, } = useTable({ columns, }); return ( <table> <thead> {getHeaderGroups().map((headerGroup) => ( <tr key={headerGroup.id}> {headerGroup.headers.map((header) => { return ( <th key={header.id}> {header.isPlaceholder ? null : ( <> {flexRender( header.column.columnDef.header, header.getContext(), )} {header.column.getCanFilter() ? ( <div> <input value={ (header.column.getFilterValue() as string) ?? "" } onChange={(e) => header.column.setFilterValue(e.target.value) } /> </div> ) : null} </> )} </th> ); })} </tr> ))} </thead> <tbody> {getRowModel().rows.map((row) => { return ( <tr key={row.id}> {row.getVisibleCells().map((cell) => { return ( <td key={cell.id}> {flexRender(cell.column.columnDef.cell, cell.getContext())} </td> ); })} </tr> ); })} </tbody> </table> ); };这个示例演示了三个关键知识点:
- 筛选 UI 由你手写:
header.column.getCanFilter()判断该列是否允许筛选(对应enableColumnFilter: false的id列就不会渲染输入框); - 受控输入绑定:输入框的
value来自header.column.getFilterValue(),onChange调用header.column.setFilterValue(); meta.filterOperator决定服务端算子:title/status用"contains",createdAt用"gte",id列明确关闭筛选。
三、从列筛选到 CrudFilters:底层翻译逻辑
TanStack Table 的筛选状态只是一个{ id, value }列表,但 Refine 的数据提供者需要的是带operator的CrudFilters(LogicalFilter或ConditionalFilter)。二者之间的翻译由适配器自动完成。
3.1 默认算子
在 packages/react-table/src/utils/column-filters-to-crud-filters/index.ts 中,翻译规则为:
- 优先读取列定义
meta.filterOperator; - 若未声明,则根据值的类型取默认:值为数组时用
"in",否则用"eq"。
测试用例 column-filters-to-crud-filters/index.spec.ts 直接验证了这一行为:{ id: "name", value: "John" }会翻译成{ field: "name", operator: "eq", value: "John" },而数组值["John", "Doe"]会被翻译成operator: "in"。
3.2 组合过滤(and / or)
当meta.filterOperator为"and"或"or",且筛选值是一个数组时,会产生ConditionalFilter结构:
{ key: filterKey, // 默认取列 id operator: "or", // 或 "and" value: [ ...子过滤条件... ], }crudFiltersToColumnFilters反向翻译时,会通过meta.filterKey找到对应的列 id(见 crud-filters-to-column-filters/index.ts)。适配器测试对「组合过滤」和「自定义filterKey」都有覆盖(index.spec.ts)。
3.3 已移除筛选的同步
当某列筛选被清空时,适配器通过getRemovedFilters把对应条件以value: undefined的形式补回,确保 Refine 侧的filters状态被完整同步(见 get-removed-filters/index.ts)。
3.4 筛选变化时的行为
在 useTable/index.ts 中,useEffect监听columnFilters,将其翻译后通过setFilters写入 Refine;若当前存在筛选且分页开启,还会自动把页码重置回第 1 页(setCurrentPage(1)),避免停留在筛选结果之外的分页上。
四、配套属性:如何控制过滤行为
以下属性都在useTable({ refineCoreProps: {...} })中配置,详见 useTable Hook 文档:
| 属性 | 取值 | 默认值 | 说明 |
|---|---|---|---|
filters.mode | "server"/"off" | "server" | "off"时不把筛选发送给服务端,配合 TanStack Table 客户端筛选(getFilteredRowModel会被启用) |
filters.initial | CrudFilter[] | — | 初始筛选值,用户修改后即被清除 |
filters.permanent | CrudFilter[] | — | 永久筛选值,不可被用户操作清除 |
filters.defaultBehavior | "merge"/"replace" | "replace" | 新筛选如何与现有筛选合并:"merge"按列合并,"replace"整体替换 |
syncWithLocation | boolean | 来自<Refine> | 开启后筛选、排序、分页状态会编码进 URL query,支持分享/书签特定表格视图 |
服务端过滤开关manualFiltering正是由filters.mode推导而来(useTable/index.ts),这解释了为什么filters.mode: "off"可以无缝切换到 TanStack 自带的客户端过滤——此时适配器不再把getFilteredRowModel禁用。
读取当前筛选值
Refine 提供了getDefaultFilter工具,从refineCore.filters中取出指定字段的筛选值:
import { getDefaultFilter } from "@refinedev/core"; import { useTable } from "@refinedev/react-table"; const MyComponent = () => { const { refineCore: { filters }, } = useTable({ refineCoreProps: { filters: { initial: [ { field: "name", operator: "contains", value: "John Doe" }, ], }, }, }); const nameFilterValue = getDefaultFilter("name", filters, "contains"); console.log(nameFilterValue); // "John Doe" return { /* ... */ }; };getDefaultFilter的实现位于核心包的src/definitions/table/index.ts(对应 packages/core 目录),适合在需要把筛选值回填到表单、下拉框或自定义筛选面板时使用。
五、实践要点与注意事项
5.1 完整示例运行环境
该 Live Preview 示例的宿主代码会在启动时执行setInitialRoutes(["/posts"]),并将PostList挂载到<RefineHeadlessDemo>下的/posts路由。你需要一个带posts资源的 Refine 应用,例如参考仓库中的 table-react-table-basic 示例(useTable文档底部即引用此示例)。resource默认从当前路由读取,也可以通过refineCoreProps.resource显式指定。
5.2 常见问题
- 为什么
id列不显示筛选框?因为设置了enableColumnFilter: false,getCanFilter()返回false; - 为什么每次输入都触发请求?默认
filters.mode: "server",列筛选状态一旦变化就会通过setFilters触发getList重新请求;如果希望纯客户端过滤,设置filters.mode: "off"即可(FAQ 章节 也推荐了这一做法); filterOperator必须是CrudOperators类型:包括eq、ne、lt、lte、gt、gte、contains、ncontains、in、nin、between、and、or等,具体以核心包的CrudOperators定义为准;- 组合过滤的
key:默认取列 id;多个相同算子并存时,用meta.filterKey区分不同条件。
六、小结
列级筛选是 TanStack Table 与 Refine 数据层整合最典型的场景:UI 完全 headless(一个<input>即可),状态同步则由@refinedev/react-table的工具函数自动完成。掌握了meta.filterOperator、filters.mode与syncWithLocation这几个核心配置,你就可以在任何 UI 库(Ant Design、MUI、Mantine、Chakra UI 或原生 HTML)之上快速构建出支持服务端过滤、可分享 URL 状态的管理后台表格。
延伸阅读:本示例是 useTable Hook 文档 中 Filtering 小节的组成部分,同系列还有分页(Pagination)、排序(Sorting)、关联数据(Relational)等 Live Preview 示例,均位于 documentation/docs/packages/tanstack-table/examples 目录下;相关可运行工程可参考 examples/table-react-table-basic。
【免费下载链接】refineA React Framework for building internal tools, admin panels, dashboards & B2B apps with unmatched flexibility.项目地址: https://gitcode.com/GitHub_Trending/re/refine
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考