Refine v5 基础表格实战:使用 @refinedev/react-table 与 Chakra UI 构建服务端驱动表格
【免费下载链接】refineA React Framework for building internal tools, admin panels, dashboards & B2B apps with unmatched flexibility.项目地址: https://gitcode.com/GitHub_Trending/re/refine
本篇技术指南围绕 Refine v5 官方示例
table-chakra-ui-basic(对应文档 basic.md)展开,讲解如何借助@refinedev/react-table适配器,在 Chakra UI 项目中直接使用 TanStack Table(React Table)的全部能力,并让排序、筛选、分页等操作全部落到服务端数据源。读完本文,你将掌握从列定义、服务端排序/筛选、分页组件到关联数据展示的完整实现路径,并理解适配器在 Refine Core 与 React Table 之间的双向同步原理。
示例定位:一张"服务端驱动"的基础表格
Refine 的核心设计之一是"headless 灵活性":它不绑定任何 UI 库,而是通过适配器把数据层能力注入你选定的组件生态。在表格领域,这一角色由@refinedev/react-table承担——它基于 TanStack Table 构建,让开发者可以直接使用 React Table 的全部特性(列配置、排序、筛选、分页、行模型等),同时这些状态变化会被同步到 Refine Core 的useTable,最终反映为对数据提供者的真实请求参数。
table-chakra-ui-basic正是这一能力在 Chakra UI 下的最小完整示例:一张 posts 列表,支持列头排序、列头筛选、分页,并且所有操作都是"服务端模式"(server-side),即数据由 API 按需返回,而非一次性拉全量后在浏览器内存中处理。
该示例的核心依赖(见 package.json):
@refinedev/react-table:Refine 与 TanStack Table 的适配层;@tanstack/react-table:底层表格引擎;@refinedev/chakra-ui:提供List、DateField、ShowButton、EditButton、DeleteButton、usePagination等 Refine 组件与 Hook;@chakra-ui/react:UI 组件库;@refinedev/simple-rest:示例数据提供者,请求https://api.fake-rest.refine.dev;@refinedev/react-router:路由提供者。
如何运行该示例
npm create refine-app@latest -- --example table-chakra-ui-basic命令会以官方模板脚手架的方式在本地生成完整项目,随后npm install并npm run dev即可启动。应用入口 main.tsx 与 App.tsx 中可以看到标准装配:ChakraProvider(主题RefineThemes.Blue)包裹<Refine>,注册routerProvider、dataProvider与posts资源,并开启syncWithLocation与warnWhenUnsavedChanges——前者保证筛选/排序/分页状态可随 URL 同步,后者在离开未保存表单时给出提示。
第一步:用 useTable 把 Refine Core 与 React Table 接通
列表页核心在 list.tsx。它从@refinedev/react-table引入useTable,并解构出两个命名空间:
const { reactTable: { getHeaderGroups, getRowModel, setOptions }, refineCore: { setCurrentPage: setCurrent, pageCount, currentPage: current, tableQuery: { data: tableData }, }, } = useTable({ columns, refineCoreProps: { sorters: { initial: [{ field: "id", order: "desc" }], }, }, });reactTable:TanStack Table 实例,负责列头渲染、行模型、排序/筛选的 UI 状态;refineCore:Refine CoreuseTable的返回值,负责分页、排序、筛选的服务端状态与数据请求。
refineCoreProps会把配置透传给 Core 的useTable。示例中通过sorters.initial设置了默认排序:按id降序。也就是说,页面首次加载时就会带上sorters[0]=id.desc这样的请求参数,配合syncWithLocation,该默认排序还会体现在 URL 中。
在适配器内部(见 packages/react-table/src/useTable/index.ts),useTable先调用 Core 的useTable拿到tableQuery、setCurrentPage、setPageSize、setSorters、setFilters、pageCount等,再调用useReactTable构建表格实例,并把服务端模式显式写入:
manualPagination: true, manualSorting: isServerSideSortingEnabled, manualFiltering: isServerSideFilteringEnabled,其中isServerSideSortingEnabled由refineCoreProps.sorters?.mode(默认"server")决定,筛选同理(默认"server")。当manualSorting/manualFiltering为true时,TanStack Table 不会在本地排序/过滤数据,而是把状态变化抛给上层,由 Refine 转成 CRUD 请求参数。这正是本示例"服务端驱动"的底层开关。
第二步:列定义(ColumnDef)
列通过React.useMemo包裹的ColumnDef<IPost>[]定义,类型模型见 interfaces/index.d.ts:
export interface IPost { id: number; title: string; content: string; status: "published" | "draft" | "rejected"; category: { id: number }; }示例共定义 6 列:id、title、status、category.id、createdAt、actions。其中值得注意的配置:
accessorKey:直接绑定数据字段;enableColumnFilter: false:该列不出现筛选入口(如id、category.id、createdAt、actions);enableSorting: false:操作列不可排序;meta:存放 Refine 扩展信息,包括filterOperator与自定义filterElement(见下文筛选部分);cell:自定义单元格渲染,可接收getValue、table等上下文。
第三步:列头排序与筛选组件
列头由getHeaderGroups()渲染,每个Th内除了标题文本,还挂载了自定义的ColumnSorter与ColumnFilter(见 columnSorter.tsx 与 columnFilter.tsx)。
ColumnSorter:切换排序方向
export const ColumnSorter: React.FC<ColumnButtonProps> = ({ column }) => { if (!column.getCanSort()) return null; const sorted = column.getIsSorted(); return ( <IconButton aria-label="Sort" size="xs" onClick={column.getToggleSortingHandler()} style={{ transform: `rotate(${sorted === "asc" ? "180" : "0"}deg)`, transition: "transform 0.25s", }} variant={sorted ? "light" : "transparent"} icon={!sorted ? <IconSelector size={18} /> : <IconChevronDown size={18} />} /> ); };逻辑要点:
- 通过
column.getToggleSortingHandler()复用 TanStack Table 的排序切换逻辑(asc → desc → 无); getIsSorted()返回"asc"/"desc"/false,据此旋转图标(升序时箭头向上旋转 180°)并切换按钮配色。
由于适配器开启了manualSorting,点击后排序状态会被同步回 Refine,最终以sorters[]参数请求服务端。
ColumnFilter:弹出式筛选菜单
ColumnFilter使用 Chakra UI 的Menu组件实现点击图标弹出筛选输入框:
- 默认渲染
Input文本输入框; - 若列在
meta.filterElement中提供了自定义筛选元素,则优先渲染该元素; - 提供"清除"(
column.setFilterValue(undefined))与"保存"(column.setFilterValue(state.value))两个操作。
在list.tsx中,title列通过meta.filterOperator: "contains"指定服务端使用包含匹配;status列则同时指定了filterOperator: "eq"和自定义filterElement——一个包含 published / draft / rejected 三个选项的下拉框:
{ id: "status", header: "Status", accessorKey: "status", meta: { filterElement: ({ value, onChange }) => ( <Select borderRadius="md" size="sm" placeholder="All Status" {...{ value, onChange }}> <option value="published">published</option> <option value="draft">draft</option> <option value="rejected">rejected</option> </Select> ), filterOperator: "eq", }, },这里的filterOperator会被适配器转换为 CRUD 筛选对象。在 column-filters-to-crud-filters 中,列筛选(columnFilters)会结合列的meta.filterOperator生成形如{ field, operator: "contains" | "eq", value }的CrudFilter,最终由数据提供者拼接为?title_like=xxx&status=published之类的查询参数。
第四步:服务端分页与自定义分页组件
分页同样走服务端模式。useTable的refineCore返回currentPage、pageCount、setCurrentPage,示例把它们传给自定义的 Pagination:
export const Pagination: FC<PaginationProps> = ({ current, pageCount, setCurrent }) => { const pagination = usePagination({ current, pageCount }); // ... };usePagination来自@refinedev/chakra-ui,根据当前页与总页数计算出一组可点击的页码条目(含...省略号),返回items、prev、next。渲染逻辑:
pagination.prev存在时渲染"上一页"图标按钮,点击setCurrent(current - 1);- 遍历
pagination.items,字符串项渲染为...,数字项渲染为页码按钮,当前页用solid变体高亮; pagination.next存在时渲染"下一页"按钮。
点击任意页码都会调用setCurrent,进而触发 Refine 以current(页码)和pageSize参数重新请求数据。在适配器内部,TanStack Table 的pagination.pageIndex变化会通过useEffect回写setCurrentPage(pageIndex + 1)(见 useTable/index.ts),这就是"点击表格自带分页状态也能驱动服务端请求"的双向同步机制。同样,排序或筛选状态变化时,适配器还会自动把页码重置为 1,避免停留在越界页(见 useTable/index.ts)。
第五步:关联数据展示(useMany + table meta)
category.id列只保存了外键 ID,显示时需要联查categories资源。示例使用useMany:
const categoryIds = tableData?.data?.map((item) => item.category.id) ?? []; const { result: categoriesData } = useMany<ICategory>({ resource: "categories", ids: categoryIds, queryOptions: { enabled: categoryIds.length > 0 }, });然后通过setOptions把categoriesData注入表格的meta:
setOptions((prev) => ({ ...prev, meta: { ...prev.meta, categoriesData }, }));单元格渲染时从table.options.meta读取这份数据并查找匹配项:
cell: ({ getValue, table }) => { const meta = table.options.meta as { categoriesData: GetManyResponse<ICategory> }; const category = meta.categoriesData?.data.find((item) => item.id === getValue()); return category?.title ?? "Loading..."; },这是 TanStack Table 官方的meta扩展点与 Refine 数据 Hook 结合的典型用法:数据通过useMany一次批量获取,避免逐行发请求;查询未返回前显示 "Loading..." 占位。
第六步:操作列与日期字段
actions列使用@refinedev/chakra-ui提供的三个按钮,通过recordItemId绑定当前行 ID,hideText只显示图标、size="sm"紧凑布局:
<HStack> <ShowButton hideText size="sm" recordItemId={getValue() as number} /> <EditButton hideText size="sm" recordItemId={getValue() as number} /> <DeleteButton hideText size="sm" recordItemId={getValue() as number} /> </HStack>这三个按钮会自动感知资源的路由定义(/posts/show/:id、/posts/edit/:id)以及删除确认与通知逻辑。createdAt列则用DateField格式化展示:
cell: ({ getValue }) => <DateField value={getValue() as string} format="LLL" />,DateField是 Refine 提供的按资源i18nlocale 渲染日期的字段组件,format="LLL"输出类似 "Sep 11, 2026 8:30 PM" 的可读格式。
适配器原理:refineCore 与 reactTable 的双向同步
抛开示例看本质,@refinedev/react-table的核心价值在于四个方向的同步(全部可在 useTable/index.ts 中验证):
- 数据流:
useReactTable({ data: tableQuery.data?.data ?? [] }),服务端返回的当前页数据作为表格数据源; - 分页同步:
useEffect监听pagination.pageIndex变化调用setCurrentPage(pageIndex + 1);pageSize变化调用setPageSizeCore; - 排序同步:
useEffect监听sorting,将其映射为CrudSorting(field+order)后setSorters,同时重置页码; - 筛选同步:
useEffect通过 column-filters-to-crud-filters 把columnFilters结合列meta.filterOperator转成CrudFilter[],再叠加 get-removed-filters 计算出的已删除筛选,最终setFilters。
反向(从 URL/外部状态恢复表格 UI)则由 crud-filters-to-column-filters 在initialState.columnFilters中完成,配合syncWithLocation,刷新页面后排序与筛选依然保留。这些工具函数都有配套单元测试(如 column-filters-to-crud-filters/index.spec.ts),可作为理解映射规则的最小文档。
另外,packages/react-table导出的适配器同样适用于其他 UI 栈(无 UI 依赖,纯 headless),Chakra UI 侧只负责把状态渲染成组件;本示例的分页复用@refinedev/chakra-ui的usePagination,若使用其他 UI 库,可自行实现同等分页逻辑。
验证与测试
仓库为每个示例都配套了 Cypress E2E 测试,本示例对应 cypress/e2e/table-chakra-ui-basic。测试覆盖了关键交互:表格行渲染、列排序点击后的服务端请求、筛选输入后的列表变化、分页跳转等,可作为"示例行为是否符合预期"的可执行验收依据。
小结
通过table-chakra-ui-basic示例可以看到,在 Refine v5 中构建一张"开箱即用"的服务端表格只需三步:
- 用
@refinedev/react-table的useTable声明列与refineCoreProps; - 用 Chakra UI 组件渲染
getHeaderGroups()与getRowModel(),并挂上排序/筛选交互; - 用
refineCore暴露的分页状态配合usePagination渲染分页器。
排序、筛选、分页全部以 CRUD 参数形式作用于数据源,关联数据用useMany+table meta优雅解决,操作列与日期字段直接复用@refinedev/chakra-ui的现成组件。理解适配器在 Core 与 TanStack Table 之间的同步机制后,这套模式可以平滑迁移到 MUI、Mantine、Ant Design 等任意 UI 生态,成为你构建管理后台列表页的通用范式。
【免费下载链接】refineA React Framework for building internal tools, admin panels, dashboards & B2B apps with unmatched flexibility.项目地址: https://gitcode.com/GitHub_Trending/re/refine
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考