Refine v5 + Chakra UI 高级表格实战:基于 @refinedev/react-table 实现筛选、排序、批量删除与行内编辑
【免费下载链接】refineA React Framework for building internal tools, admin panels, dashboards & B2B apps with unmatched flexibility.项目地址: https://gitcode.com/GitHub_Trending/re/refine
本指南以 Refine 仓库中的 Chakra UI Advanced React Table 示例(关联文档)为核心,完整讲解如何基于@refinedev/react-table适配器在 Chakra UI 项目中构建可投入真实管理后台的高阶表格。读完你将掌握:表格列的声明式定义、列筛选与列排序、行多选与批量删除、行展开、行内编辑以及分页组件的完整实现方案。
示例定位:在 Basic 表格之上叠加生产级能力
该示例对应的项目位于 examples/table-chakra-ui-advanced,其文档标题为 "Advanced React Table Example | Pagination in Refine v5",明确说明这是 Chakra UI 体系下的高级表格示例。与基础版 table-chakra-ui-basic 相比,它在原有"列表展示 + 单行操作"的基础上,将删除、编辑、筛选三大能力整合进同一张表格:支持列级筛选、列级排序、表头多选与批量删除、点击行内编辑按钮就地渲染编辑表单、展开行查看详情等真实后台高频交互。
从技术选型看,该示例并非自行封装表格逻辑,而是基于 Refine 官方表格适配器:
@refinedev/react-table(v6.0.1):Refine 对 TanStack React Table(@tanstack/react-tablev8.2.6)的适配层,负责把 Refine 的数据获取、分页、排序、筛选状态与 TanStack 表格状态同步;@refinedev/corev5.0.12:提供useTable底层的数据请求、useDeleteMany、useMany、useSelect等核心 hooks;@refinedev/react-hook-formv5.0.4:为行内编辑提供表单能力;@refinedev/chakra-uiv3.0.2:提供List、EditButton、DeleteButton、SaveButton、DateField等 UI 组件与usePagination。
依赖清单可在 examples/table-chakra-ui-advanced/package.json 中确认,其脚本为标准的refine dev/refine build/refine start,Node 环境要求>=20。
应用骨架:Refine + Chakra UI 的最小装配
在进入表格实现之前,先看示例的入口装配 src/App.tsx,它演示了 Refine 应用的标准初始化方式:
import { GitHubBanner, Refine } from "@refinedev/core"; import { ErrorComponent, ThemedLayout, RefineThemes, useNotificationProvider, } from "@refinedev/chakra-ui"; import { ChakraProvider } from "@chakra-ui/react"; import dataProvider from "@refinedev/simple-rest"; import routerProvider, { NavigateToResource, UnsavedChangesNotifier, DocumentTitleHandler, } from "@refinedev/react-router"; import { BrowserRouter, Routes, Route, Outlet } from "react-router";核心配置如下:
<ChakraProvider theme={RefineThemes.Blue}> <Refine notificationProvider={useNotificationProvider()} routerProvider={routerProvider} dataProvider={dataProvider("https://api.fake-rest.refine.dev")} resources={[ { name: "posts", list: "/posts" }, ]} options={{ syncWithLocation: true, warnWhenUnsavedChanges: true, }} > <Routes> <Route element={ <ThemedLayout> <Outlet /> </ThemedLayout> } > <Route index element={<NavigateToResource resource="posts" />} /> <Route path="/posts" element={<PostList />} /> <Route path="*" element={<ErrorComponent />} /> </Route> </Routes> <UnsavedChangesNotifier /> <DocumentTitleHandler /> </Refine> </ChakraProvider>几个值得注意的要点:
- 数据提供器:示例使用
@refinedev/simple-rest连接公开的https://api.fake-rest.refine.devREST API,这意味着表格的排序、筛选、分页请求都由 Refine 自动转换成带查询参数的 HTTP 请求发出; - 主题:通过
ChakraProvider theme={RefineThemes.Blue}注入 Refine 为 Chakra UI 预设的主题; - 路由与布局:
ThemedLayout提供统一的侧边栏与头部布局,/posts路由挂载PostList表格页面; - 体验细节:
syncWithLocation: true让筛选/排序/分页状态同步到 URL(便于分享与刷新还原);warnWhenUnsavedChanges: true在行内编辑未保存时离开页面会给出提示,配合UnsavedChangesNotifier生效。
核心 hooks 组合:useTable 与 useForm 的分工
高级表格的"高级"之处,首先体现在它同时使用了两套表单/表格体系:
| Hook | 来源 | 职责 |
|---|---|---|
useTable | @refinedev/react-table | 表格数据、列定义、排序、筛选、分页、行选择、行展开状态 |
useForm | @refinedev/react-hook-form | 行内编辑表单的字段注册、校验与提交 |
页面主体 src/pages/posts/list.tsx 中,表单与表格各自解构出自己的状态:
const { refineCore: { onFinish, id, setId }, saveButtonProps, handleSubmit, register, } = useForm<IPost>({ refineCoreProps: { redirect: false, action: "edit", }, });action: "edit"表示表单以编辑模式工作;redirect: false表示提交后不跳转,而是停留在表格页并关闭编辑行——这正是行内编辑的典型诉求。id与setId控制"当前正在编辑哪一行":当setId被设置为某行 ID 时,该行渲染为编辑态,见下文行内编辑小节。
声明式列定义:从数据列到交互列的完整映射
表格的骨架是React.useMemo包裹的ColumnDef<IPost>[]列数组。类型定义见 src/interfaces/index.d.ts:
export interface IPost { id: number; title: string; content: string; status: "published" | "draft" | "rejected"; category: { id: number }; }列定义覆盖了七类典型场景:
1. 选择列(selection):禁用排序与筛选,表头渲染全选Checkbox,并利用table.getIsAllRowsSelected()/table.getIsSomeRowsSelected()驱动"全选"与"部分选中"的视觉状态;行单元格渲染单行多选框与展开按钮(row.toggleExpanded()),这是后续批量删除与行展开的入口。
2. 普通数据列(id、createdAt):createdAt列通过 Refine 的DateField组件格式化显示(format="LLL"),并关闭该列筛选。
3. 文本筛选列(title):通过meta.filterOperator: "contains"声明该列使用"包含"筛选算子:
{ id: "title", header: "Title", accessorKey: "title", meta: { filterOperator: "contains" }, },4. 自定义筛选元素列(status):不满足于默认的文本输入框,示例通过meta.filterElement返回一个 ChakraSelect下拉框,配合filterOperator: "eq"实现精确匹配筛选:
{ id: "status", header: "Status", accessorKey: "status", meta: { filterElement: function render(props: FilterElementProps) { return ( <Select borderRadius="md" size="sm" placeholder="All Status" {...props}> <option value="published">published</option> <option value="draft">draft</option> <option value="rejected">rejected</option> </Select> ); }, filterOperator: "eq", }, },5. 关联数据解析列(category.id):该列存储的是外键category.id,单元格渲染时从table.options.meta.categoriesData中查找对应分类标题(找不到时显示 "Loading..."),实现"外键 → 可读文本"的转换,数据来源见后文useMany小节。
6. 操作列(actions):每行渲染EditButton(点击触发setId(getValue() as number)进入行内编辑)与DeleteButton(传入recordItemId执行单行删除),并关闭筛选与排序。
将列声明交给useTable时,示例还设置了初始排序:
const { ... } = useTable({ columns, refineCoreProps: { sorters: { initial: [{ field: "id", order: "desc" }], }, }, });即首次加载按id降序排列。
列筛选与列排序:两个可复用组件
示例将排序与筛选按钮抽成了通用组件,位于 src/components/table,在表头中与列标题并排渲染:
<HStack spacing="2"> <ColumnSorter column={header.column} /> <ColumnFilter column={header.column} /> </HStack>ColumnSorter(columnSorter.tsx):通过column.getCanSort()判断该列是否允许排序,点击触发column.getToggleSortingHandler();图标依据排序状态切换——升序显示IconChevronDown、降序显示IconChevronUp、未排序显示IconSelector。
ColumnFilter(columnFilter.tsx):核心逻辑是:
column.getCanFilter()为假(如enableColumnFilter: false的列)时直接返回null;- 通过 Chakra
Menu弹出筛选面板; - 若列声明了
meta.filterElement则渲染自定义筛选组件(如 status 列的Select),否则回退到默认Input文本框; - 面板内提供"清除"(
column.setFilterValue(undefined))与"保存"(column.setFilterValue(state.value))两个按钮,确认后才真正写入筛选值并触发 Refine 重新请求数据。
这两个组件在基础版示例 table-chakra-ui-basic 中同样被复用,说明它们是与业务无关的通用表格能力,可以在不同资源间自由迁移。
多选与批量删除:useDeleteMany 的接入方式
批量删除由表头全选勾选框与useDeleteMany协同完成。页面中先获取删除函数:
const { mutate } = useDeleteMany<IPost>(); const deleteSelectedItems = (ids: number[]) => { mutate( { resource: "posts", ids }, { onSuccess: () => { resetRowSelection(); }, }, ); };表头的 Delete 按钮仅在存在选中行时渲染(table.getIsSomeRowsSelected() || table.getIsAllRowsSelected()),点击后收集所有选中行 ID 并调用deleteSelectedItems:
deleteSelectedItems( table.getSelectedRowModel().flatRows.map(({ original }) => original.id), );删除成功(onSuccess)后调用resetRowSelection()清空行选择状态,避免残留幽灵勾选。useDeleteMany来自@refinedev/core,底层会向数据提供器发送批量删除请求(REST 场景下通常是DELETE /posts/{id}逐条或批量端点),并自动触发列表数据刷新。
行展开与行内编辑:一张表里的两种渲染态
这是该示例最具"高级感"的部分:同一张表在不同行上同时存在"展示态"与"编辑态"。
- 行展开:行首展开按钮调用
row.toggleExpanded(),展开后在行下方插入一行colSpan横跨所有列的只读Textarea,展示该帖子的content全文:
{row.getIsExpanded() && ( <Tr id="expanded-row"> <Td colSpan={row.getVisibleCells().length}> <Textarea readOnly value={row.original.content} /> </Td> </Tr> )}- 行内编辑:通过
id === (row.original as IPost).id判断当前行是否为编辑目标行,若是则调用renderEditRow(row)渲染编辑态行。编辑态行包含title、status、category.id三个受控输入(通过register(...)注册到react-hook-form)以及保存/取消按钮:
const renderEditRow = useCallback( (row: Row<IPost>) => { const { id } = row.original; return ( <React.Fragment key={id}> <Tr> {/* ... */} <Td> <Input id="title-input" {...register("title")} /> </Td> <Td> <Select {...register("status")}> <option value="published">published</option> <option value="draft">draft</option> <option value="rejected">rejected</option> </Select> </Td> <Td> <Select {...register("category.id")}> {options.map((item) => ( <option key={item.value} value={Number(item.value)}> {item.label} </option> ))} </Select> </Td> {/* Save / Cancel 按钮 */} </Tr> <Tr> <Td colSpan={getAllColumns().length}> <Textarea {...register("content")} /> </Td> </Tr> </React.Fragment> ); }, [options], );整个表格被包裹在一个<form onSubmit={handleSubmit(onFinish)}>中,保存按钮即表单提交按钮,提交后由useForm的onFinish触发更新请求(PATCH /posts/{id}),随后setId被清空、该行回归展示态。分类下拉选项由useSelect提供(resource: "categories",pageSize: 9999一次性拉取全部,mode: "server"服务端分页模式),其数据结构{ value, label }与<option>直接对应。
关联数据预取:useMany 与 setOptions 的配合
由于category.id列需要展示分类名称,示例通过两条路径获取分类数据:
- 当前页关联数据:从表格数据中收集当前页涉及的所有
category.id,调用useMany批量查询:
const categoryIds = tableData?.data?.map((item) => item.category.id) ?? []; const { result: categoriesData } = useMany<ICategory>({ resource: "categories", ids: categoryIds, queryOptions: { enabled: categoryIds.length > 0 }, });- 注入表格 meta:通过
setOptions将查询结果塞回 TanStack 表格的meta,供 category 列的单元格渲染函数读取:
setOptions((prev) => ({ ...prev, meta: { ...prev.meta, categoriesData, }, }));这种做法把"关联数据解析"从渲染闭包中解耦,table.options.meta.categoriesData成为列内共享的数据通道。同时,编辑态行使用的分类选项则来自useSelect,二者分工明确:useMany负责展示解析,useSelect负责编辑选择。
分页:usePagination 驱动的页码条
示例没有直接使用 TanStack 的默认分页 UI,而是基于@refinedev/chakra-ui的usePagination封装了自定义分页条 src/components/pagination/index.tsx:
const pagination = usePagination({ current, pageCount });current、pageCount、setCurrent均来自useTable解构出的refineCore字段(currentPage别名current,setCurrentPage别名setCurrent)。组件渲染"上一页 / 页码(含省略号...)/ 下一页"结构,页码点击调用setCurrent(page)触发 Refine 重新请求对应页数据。
{pagination?.items.map((page) => { if (typeof page === "string") return <span key={page}>...</span>; return ( <Button key={page} onClick={() => setCurrent(page)} variant={page === current ? "solid" : "outline"} > {page} </Button> ); })}pagination.prev/pagination.next为布尔值,控制首尾翻页按钮是否渲染,从而在首页/末页自动隐藏不可用的翻页方向。
端到端测试:功能行为的验证依据
该示例的交互行为有 Cypress 端到端测试背书,见 cypress/e2e/table-chakra-ui-advanced/all.cy.ts,覆盖了前文描述的四大核心能力:
- 行展开:点击
.tabler-icon-chevron-right后断言#expanded-row出现; - 全选:点击表头
Checkbox后断言所有#row-select勾选框均被选中; - 批量删除按钮显隐:初始断言
#delete-selected不存在,勾选任意一行后断言其出现; - 行内编辑:点击编辑按钮后断言表单被当前行数据填充(
#title-input的值为接口返回的title),修改并保存后断言PATCH请求体中的title已更新。
这些测试与 list.tsx 中#expanded-row、#delete-selected、#row-select、#title-input等 DOM 标识一一对应,可以作为你自行扩展表格功能时的回归测试模板。
运行方式
在本地运行该示例(源自 examples/table-chakra-ui-advanced/README.md):
npm create refine-app@latest -- --example table-chakra-ui-advanced进入项目目录后执行pnpm install(仓库使用 pnpm 管理依赖)再运行pnpm dev即可启动开发服务器,随后访问/posts页面体验:点击列头排序图标切换升降序、点击筛选图标对 Title 做包含匹配或对 Status 做下拉精确匹配、勾选多行批量删除、点击行首箭头展开全文、点击行内 Edit 就地修改并保存。整个过程的网络请求会以带查询参数的 REST 请求形式打到https://api.fake-rest.refine.dev/posts,便于在浏览器 DevTools 中直观理解 Refine 表格状态如何被翻译为后端查询。
小结
Chakra UI Advanced Table 示例是 Refine v5 表格能力的浓缩展示:@refinedev/react-table负责把 TanStack React Table 的状态模型与 Refine 的数据层无缝桥接,@refinedev/react-hook-form让行内编辑与 Refine 的 mutation 流程对接,useMany/useSelect解决关联数据展示与编辑选项,而usePagination、ColumnFilter、ColumnSorter则提供了可复用的交互基建。当你需要在一个真实管理后台中同时满足"筛选、排序、多选删除、行内编辑、展开详情"这些需求时,本文描述的这套组件结构与 hooks 组合可以直接迁移到自己的 Refine 项目中。
【免费下载链接】refineA React Framework for building internal tools, admin panels, dashboards & B2B apps with unmatched flexibility.项目地址: https://gitcode.com/GitHub_Trending/re/refine
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考