news 2026/9/12 20:38:05

使用 Refine v5 与 @refinedev/react-table 构建高级 TanStack Table:行内编辑、批量删除与服务端筛选实战

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
使用 Refine v5 与 @refinedev/react-table 构建高级 TanStack Table:行内编辑、批量删除与服务端筛选实战

使用 Refine v5 与 @refinedev/react-table 构建高级 TanStack Table:行内编辑、批量删除与服务端筛选实战

【免费下载链接】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-react-table-advanced为骨架,讲解如何基于@refinedev/react-table适配器在真实项目中组合 TanStack Table 的高级能力:行内编辑、行选择与批量删除、按列筛选、排序、展开行渲染子组件以及完整分页。读者学完后,可以直接复刻该示例,在自己的 Refine 应用中实现一个功能完整的可编辑数据表格。

示例概述与运行方式

原文档(documentation/docs/examples/table/tanstack-table/advanced.md)指出:该示例是使用 Refine 的@refinedev/react-table适配器创建的Advanced TanStack Table示例。与 Basic 示例相比,它在表格中同时集成了删除、编辑与筛选三大功能。

完整源码位于仓库 examples/table-react-table-advanced 目录,其技术栈(见 package.json)包括:

  • @refinedev/core^5.0.12 与@refinedev/react-table^6.0.1(Refine 核心与表格适配器)
  • @tanstack/react-table^8.2.6(表格引擎)
  • @refinedev/react-hook-form^5.0.4 与react-hook-form^7.57.0(行内编辑表单)
  • @refinedev/simple-rest^6.0.1(REST 数据提供器)
  • @uiw/react-md-editor(Markdown 编辑器,用于展示与编辑内容列)
  • react-router^7.0.2(路由)

本地运行该示例有两种方式:

# 方式一:通过 create-refine-app 脚手架直接拉取示例 npm create refine-app@latest -- --example table-react-table-advanced
# 方式二:进入仓库内示例目录直接启动 cd examples/table-react-table-advanced npm install npm run dev # 内部等价于 refine dev

示例默认连接https://api.fake-rest.refine.dev提供的 posts/categories 模拟数据接口,可直接体验完整的表格交互。

应用入口:最小化的 Refine 配置

在 src/App.tsx 中,应用仅注册了posts一个资源,并启用了两个实用选项:

const API_URL = "https://api.fake-rest.refine.dev"; <Refine routerProvider={routerProvider} dataProvider={dataProvider(API_URL)} resources={[ { name: "posts", list: "/posts", }, ]} options={{ syncWithLocation: true, // 表格状态(筛选/排序/分页)与 URL 同步 warnWhenUnsavedChanges: true, // 未保存编辑离开页面时给出警告 }} > <Routes> <Route index element={<NavigateToResource />} /> <Route path="/posts" element={<PostList />} /> <Route path="*" element={<ErrorComponent />} /> </Routes> <UnsavedChangesNotifier /> <DocumentTitleHandler /> </Refine>

值得关注的是syncWithLocation: true:它让 TanStack Table 的筛选、排序与分页状态自动同步到 URL 查询参数,用户刷新或分享链接后表格状态不会丢失,这是进阶后台表格很实用的体验细节。

核心列表组件:useTable 的返回值解构

PostList组件(src/pages/posts/list.tsx)是整篇示例的核心。它从useTable中同时解构出reactTablerefineCore两个命名空间:

const { reactTable: { setOptions, getColumn, getAllColumns, getHeaderGroups, getRowModel, setPageIndex, setPageSize, getState, getCanPreviousPage, getPageCount, getCanNextPage, nextPage, previousPage, resetRowSelection, }, refineCore: { tableQuery: { data: tableData }, }, } = useTable<IPost>({ columns, getRowId: (originalRow) => originalRow.id.toString(), });

这种返回结构的来源可以在 packages/react-table/src/useTable/index.ts 的实现中验证:useTable内部同时调用@refinedev/coreuseTableCore管理数据获取,并调用@tanstack/react-tableuseReactTable管理表格状态,最终返回{ reactTable, refineCore }。数据模型定义在 src/interfaces/index.d.ts:

export interface ICategory { id: number; title: string; } export interface IPost { id: number; title: string; content: string; status: "published" | "draft" | "rejected"; category: { id: number }; }

getRowId被设置为使用id作为行唯一标识,这保证了删除、编辑、行选择等操作能精确映射到数据记录。

列定义:筛选、排序、选择与行内操作

columns通过React.useMemo定义,类型为ColumnDef<IPost>[],包含 5 列:

1. 选择列(selection)

该列实现全选/半选(indeterminate)复选框与行展开开关:

{ id: "selection", accessorKey: "id", enableSorting: false, // 禁用排序 header: function render({ table }) { return ( <> <IndeterminateCheckbox {...{ checked: table.getIsAllRowsSelected(), indeterminate: table.getIsSomeRowsSelected(), onChange: table.getToggleAllRowsSelectedHandler(), }} />{" "} {table.getIsSomeRowsSelected() && ( <button id="delete-selected" type="button" onClick={() => deleteSelectedItems( table.getSelectedRowModel().flatRows.map(({ original }) => original.id), )}> Delete </button> )} </> ); }, cell: function render({ row }) { return ( <> <IndeterminateCheckbox {...{ checked: row.getIsSelected(), indeterminate: row.getIsSomeSelected(), onChange: row.getToggleSelectedHandler(), }} /> <span id="expand-toggle" onClick={() => row.toggleExpanded()}> {row.getIsExpanded() ? "👇" : "👉"} </span> </> ); }, }

其中IndeterminateCheckbox是示例自定义的受控复选框组件,通过useRefuseEffect手动设置indeterminate属性,实现"部分选中"的视觉状态。当有行被选中时,表头自动出现Delete按钮,将选中行的 id 数组交给deleteSelectedItems批量删除。

2. 标题列(title)与分类列(category.id)

这两列的关键在于meta.filterOperator声明,它告诉 Refine 如何把 TanStack Table 的列筛选转换为数据提供器的 CRUD 筛选:

{ id: "title", header: "Title", accessorKey: "title", meta: { filterOperator: "contains" }, // 模糊匹配 }, { id: "category.id", header: "Category", accessorKey: "category.id", cell: function render({ 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..."; }, meta: { filterOperator: "eq" }, // 精确匹配 },

分类列本身只存储category.id,显示时通过table.options.meta.categoriesData(由useMany一次性获取的分类字典)反查标题。meta.filterOperator的映射逻辑由 packages/react-table/src/utils/column-filters-to-crud-filters 与 packages/react-table/src/utils/crud-filters-to-column-filters 两个工具负责双向转换——这正是 Refine 把 TanStack Table 的本地筛选状态与"服务端筛选"对接的底层机制。

3. 操作列(actions)

{ id: "actions", header: "Actions", accessorKey: "id", cell: function render({ getValue }) { return ( <div> <button type="button" onClick={() => handleEditButtonClick(getValue() as number)}> Edit </button> <button type="button" onClick={() => deleteSelectedItems([getValue() as number])}> Delete </button> </div> ); }, }
  • Edit:调用handleEditButtonClick,内部执行setId(editId),把该行切换为行内编辑模式;
  • Delete:复用deleteSelectedItems传入单元素数组,实现单行删除。

行内编辑:useForm 与展开行渲染的结合

行内编辑是本示例最核心的高级特性,它由@refinedev/react-hook-formuseForm驱动:

const { refineCore: { onFinish, id, setId }, register, handleSubmit, control, } = useForm<IPost>({ refineCoreProps: { redirect: false, // 提交后不跳转,留在列表页 action: "edit", // 表单用于编辑 }, });

setId同时控制"哪一行进入编辑状态":在渲染表格体时,若id === (row.original as IPost).id,则用renderEditRow(row)替换该行:

{getRowModel().rows.map((row) => { if (id === (row.original as IPost).id) { return renderEditRow(row); } return ( <React.Fragment key={row.id}> <tr>...正常单元格渲染...</tr> {row.getIsExpanded() ? ( <tr id="expanded-row"> <td colSpan={row.getVisibleCells().length}> {renderRowSubComponent({ row })} </td> </tr> ) : null} </React.Fragment> ); })}

renderEditRow渲染两行结构:第一行是title输入框、category.id下拉框(选项来自useSelectoptions)、Save/Cancel 按钮;第二行colSpan占满所有列,通过react-hook-formController渲染MDEditor编辑content字段:

const renderEditRow = useCallback((row: Row<IPost>) => { const { id, title, content } = row.original; return ( <> <tr key={`edit-${id}-inputs`}> <td>...展开开关...</td> <td><span>{id}</span></td> <td> <input id="title-input" type="text" defaultValue={title} {...register("title", { required: "Title is required" })} /> </td> <td> <select id="category-select" {...register("category.id", { required: "Category title is required" })}> {options?.map((category) => ( <option key={category.value} value={category.value}> {category.label} </option> ))} </select> </td> <td> <button type="submit">Save</button> <button onClick={() => setId(undefined)}>Cancel</button> </td> </tr> <tr key={`edit-${id}-mde`}> <td colSpan={getAllColumns().length}> <Controller defaultValue={content} control={control} name="content" rules={{ required: "Content is required" }} render={({ field: { onChange, value } }) => ( <MDEditor value={value} onChange={onChange}>const { mutate } = useDeleteMany<IPost>(); const deleteSelectedItems = (ids: number[]) => { mutate( { resource: "posts", ids, }, { onSuccess: () => { resetRowSelection(); // 删除成功后清空行选择 }, }, ); };
  • 单行删除:deleteSelectedItems([getValue() as number])
  • 批量删除:从table.getSelectedRowModel().flatRows取出全部选中行的original.id组成数组传入。

删除成功后调用 TanStack Table 的resetRowSelection()清空选择状态,避免残留脏选择。

关联数据与筛选联动

表格的 Category 列同时用到两组数据源:

// 1) 用 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 注入 table 的 meta,供列 cell 读取 setOptions((prev) => ({ ...prev, meta: { ...prev.meta, categoriesData }, })); // 2) 用 useSelect 获取完整分类选项,用于筛选下拉与编辑行下拉 const { options } = useSelect<ICategory>({ resource: "categories", defaultValue: categoryIds, pagination: { mode: "server" }, });
  • useMany是"按需取数":只请求当前页出现过的 category id,结果通过setOptions注入table.options.meta,列 cell 再从 meta 中反查标题;
  • useSelect提供{ value, label }[]形式的完整选项列表,同时服务于顶部筛选下拉和编辑行内的分类下拉。

顶部的两个筛选控件通过getColumn(...).setFilterValue与 TanStack Table 的列筛选状态绑定:

const titleColumn = getColumn("title"); const categoryColumn = getColumn("category.id"); <input id="title" type="text" value={(titleColumn?.getFilterValue() as string) ?? ""} onChange={(event) => titleColumn?.setFilterValue(event.target.value)} /> <select id="category" aria-label="Category select" value={(categoryColumn?.getFilterValue() as string) ?? ""} onChange={(event) => categoryColumn?.setFilterValue(event.target.value)} > <option value={[]}>All Categories</option> {options?.map((category) => ( <option key={category.value} value={category.value || undefined}> {category.label} </option> ))} </select>

由于 Refine 的useTable默认使用服务端筛选模式,setFilterValue触发的列筛选会经columnFiltersToCrudFilters转换为CrudFilter[]并重新请求数据。服务端/客户端模式的判定逻辑在 packages/react-table/src/useTable/index.ts 中:

const isServerSideFilteringEnabled = (refineCoreProps.filters?.mode || "server") === "server"; const isServerSideSortingEnabled = (refineCoreProps.sorters?.mode || "server") === "server"; // 服务端模式时不挂载本地 getFilteredRowModel / getSortedRowModel getFilteredRowModel: isServerSideFilteringEnabled ? undefined : getFilteredRowModel(), getSortedRowModel: isServerSideSortingEnabled ? undefined : getSortedRowModel(),

这意味着示例中的筛选与排序都由 Refine 转发给数据提供器在服务端完成,适合大数据集场景;若想改造成纯客户端筛选,将filters.modesorters.mode设为"client"即可。

展开行:Markdown 内容预览子组件

通过行选择列中的row.toggleExpanded()开关,点击👉/👇可以展开/收起内容预览行。展开后渲染renderRowSubComponent,用MDEditor.Markdown渲染文章的content字段:

const renderRowSubComponent = useCallback( ({ row }: { row: Row<IPost> }) => ( <div><div className="pagination"> <button type="button" onClick={() => setPageIndex(0)} disabled={!getCanPreviousPage()}>{"<<"}</button> <button type="button" onClick={() => previousPage()} disabled={!getCanPreviousPage()}>{"<"}</button> <button type="button" onClick={() => nextPage()} disabled={!getCanNextPage()}>{">"}</button> <button type="button" onClick={() => setPageIndex(getPageCount() - 1)} disabled={!getCanNextPage()}>{">>"}</button> <span> Page <strong>{getState().pagination.pageIndex + 1} of {getPageCount()}</strong>{" "} </span> <span> | Go to page:{" "} <input type="number" defaultValue={getState().pagination.pageIndex + 1} onChange={(e) => { const page = e.target.value ? Number(e.target.value) - 1 : 0; setPageIndex(page); }} style={{ width: "100px" }} /> </span>{" "} <select value={getState().pagination.pageSize} onChange={(e) => setPageSize(Number(e.target.value))}> {[10, 20, 30, 40, 50].map((pageSize) => ( <option key={pageSize} value={pageSize}>Show {pageSize}</option> ))} </select> </div>

分页的底层机制同样可以在 packages/react-table/src/useTable/index.ts 中找到证据:适配器将 Refine 核心的分页状态(currentPagepageSizepageCount)映射进useReactTableinitialState.pagination,并设置manualPagination: truepageCount,让分页完全由 Refine 的服务端分页驱动:

initialState: { pagination: { pageIndex: currentPage - 1, pageSize: pageSizeCore, }, sorting: sorters.map((sorting) => ({ id: sorting.field, desc: sorting.order === "desc", })), columnFilters: crudFiltersToColumnFilters({ columns: rest.columns, crudFilters: filtersCore, }), ...reactTableInitialState, }, pageCount, manualPagination: true, manualSorting: isServerSideSortingEnabled, manualFiltering: isServerSideFilteringEnabled,

同时支持页码直接跳转(setPageIndex)与每页条数切换(setPageSize,可选 10/20/30/40/50)。

表头排序

表头通过header.column.getToggleSortingHandler()绑定点击排序,并用header.column.getIsSorted()渲染排序指示符:

<th key={header.id} colSpan={header.colSpan} onClick={header.column.getToggleSortingHandler()} > {flexRender(header.column.columnDef.header, header.getContext())} {{ asc: " 🔼", desc: " 🔽", }[header.column.getIsSorted() as string] ?? null} </th>

由于默认采用服务端排序(manualSorting: true),点击表头后 Refine 会更新sorters并重新请求数据,排序状态同样受syncWithLocation管理。

小结:从示例到生产实践

table-react-table-advanced示例(examples/table-react-table-advanced)展示了一整套可复用的高级表格范式:

能力关键技术仓库证据
表格引擎与数据层解耦@refinedev/react-table包装@tanstack/react-tablepackages/react-table/src/useTable/index.ts
行内编辑useForm+setId切换编辑行 + 受控表单src/pages/posts/list.tsx
批量删除useDeleteMany+ 行选择状态同上
服务端筛选meta.filterOperator→ CRUD 筛选转换packages/react-table/src/utils/column-filters-to-crud-filters
关联数据渲染useMany注入table.options.metasrc/pages/posts/list.tsx
展开行内容预览toggleExpanded+ 子组件渲染同上
服务端分页/排序manualPagination+ 状态映射packages/react-table/src/useTable/index.ts

实际生产中,你可以沿用同样的结构:把columns定义抽取为独立模块、用useMany优化关联字段显示、将筛选/排序状态交给syncWithLocation与 URL 同步,并把编辑表单替换为useDrawerFormuseModalForm等更复杂的交互形态。所有能力都建立在 Refine 数据提供器之上,替换数据源(simple-rest、Supabase、Strapi 等)时表格代码无需改动。

【免费下载链接】refineA React Framework for building internal tools, admin panels, dashboards & B2B apps with unmatched flexibility.项目地址: https://gitcode.com/GitHub_Trending/re/refine

创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/9/12 20:33:07

2026用AI做问卷确实省时间,但题目质量还得自己把关

测评说明&#xff1a;本文为调研从业者实测记录&#xff0c;记录 2026 年四款线上问卷工具公开 AI 出题相关功能参数&#xff0c;面向 AI 辅助问卷设计场景&#xff0c;记录各渠道可支持能力。仅陈述客观功能与规则&#xff0c;不作优劣判定与选型推荐。测试基于各平台公开标准…

作者头像 李华
网站建设 2026/9/12 20:32:34

凝练了上百篇顶刊的 GPT-5.6 论文润色方法,效果极佳!

各位同仁好,我是七哥。一个在高校里从事人工智能 相关领域研究,钻研用大模型AI实操的学术人。可以和七哥交流学术写作或Gemini、GPT、Claude 等大模型 学术实操相关问题,多多交流,相互成就,共同进步。 如果一篇论文的核心观点足够扎实,却因为语言表达不够精准、逻辑衔…

作者头像 李华
网站建设 2026/9/12 20:32:00

手机日志与设备调试工具

Android / iOS 双平台日志调试工具&#xff0c;抓日志再也不用敲命令了 GitHub 地址&#xff1a;https://github.com/carterking888/phone_log_tool.git 做安卓 / iOS 测试或开发的同学大概都有这种体验&#xff1a;排查问题时要一边敲 adb logcat、adb shell、ls、pull&#…

作者头像 李华
网站建设 2026/9/12 20:29:13

从公网直连到 XS advanced 信任链,SAP HANA 本地数据库接入 Self-Service Migration 的完整技术逻辑

做 SAP HANA Platform 本地数据库迁往 SAP HANA Cloud 时,真正容易卡住项目的地方,往往不是数据库对象能不能导出,也不是目标端实例有没有创建好,而是源数据库怎样被迁移服务稳定、安全地访问。很多团队看到 Self-Service Migration,就会下意识认为本地系统必须先部署 SAP…

作者头像 李华
网站建设 2026/9/12 20:22:50

MEMS陀螺仪在卫星姿态控制中的应用与实践

1. MEMS陀螺卫星应用技术手册概述在卫星姿态控制领域&#xff0c;MEMS陀螺仪正逐步取代传统机械陀螺成为主流选择。这本技术手册将系统性地介绍如何构建完整的MEMS陀螺卫星应用生态&#xff0c;并深入解析影响系统性能的关键指标参数。不同于实验室环境下的理想测试数据&#x…

作者头像 李华
网站建设 2026/9/12 20:21:56

气象数据质量在金融交易中的关键作用与验证方法

1. 极端天气预警中的气象数据质量陷阱在金融交易领域&#xff0c;极端天气事件往往带来巨大的市场波动和资产价格重估。过去十年间&#xff0c;我参与过多次飓风、洪水和极端气温事件期间的交易决策&#xff0c;最深刻的教训是&#xff1a;预警信号的及时性只是基础门槛&#x…

作者头像 李华