尧图精选

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

🕒 发布时间:2026/9/12 20:38:14 📁 来源:尧图网络
使用 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.1Refine 核心与表格适配器tanstack/react-table^8.2.6表格引擎refinedev/react-hook-form^5.0.4 与react-hook-form^7.57.0行内编辑表单refinedev/simple-rest^6.0.1REST 数据提供器uiw/react-md-editorMarkdown 编辑器用于展示与编辑内容列react-router^7.0.2路由本地运行该示例有两种方式# 方式一通过 create-refine-app 脚手架直接拉取示例 npm create refine-applatest -- --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中同时解构出reactTable与refineCore两个命名空间const { reactTable: { setOptions, getColumn, getAllColumns, getHeaderGroups, getRowModel, setPageIndex, setPageSize, getState, getCanPreviousPage, getPageCount, getCanNextPage, nextPage, previousPage, resetRowSelection, }, refineCore: { tableQuery: { data: tableData }, }, } useTableIPost({ columns, getRowId: (originalRow) originalRow.id.toString(), });这种返回结构的来源可以在 packages/react-table/src/useTable/index.ts 的实现中验证useTable内部同时调用refinedev/core的useTableCore管理数据获取并调用tanstack/react-table的useReactTable管理表格状态最终返回{ reactTable, refineCore }。数据模型定义在 src/interfaces/index.d.tsexport 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定义类型为ColumnDefIPost[]包含 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 iddelete-selected typebutton 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 idexpand-toggle onClick{() row.toggleExpanded()} {row.getIsExpanded() ? : } /span / ); }, }其中IndeterminateCheckbox是示例自定义的受控复选框组件通过useRef与useEffect手动设置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: GetManyResponseICategory; }; 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 typebutton onClick{() handleEditButtonClick(getValue() as number)} Edit /button button typebutton onClick{() deleteSelectedItems([getValue() as number])} Delete /button /div ); }, }Edit调用handleEditButtonClick内部执行setId(editId)把该行切换为行内编辑模式Delete复用deleteSelectedItems传入单元素数组实现单行删除。行内编辑useForm 与展开行渲染的结合行内编辑是本示例最核心的高级特性它由refinedev/react-hook-form的useForm驱动const { refineCore: { onFinish, id, setId }, register, handleSubmit, control, } useFormIPost({ 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 idexpanded-row td colSpan{row.getVisibleCells().length} {renderRowSubComponent({ row })} /td /tr ) : null} /React.Fragment ); })}renderEditRow渲染两行结构第一行是title输入框、category.id下拉框选项来自useSelect的options、Save/Cancel 按钮第二行colSpan占满所有列通过react-hook-form的Controller渲染MDEditor编辑content字段const renderEditRow useCallback((row: RowIPost) { const { id, title, content } row.original; return ( tr key{edit-${id}-inputs} td...展开开关.../td tdspan{id}/span/td td input idtitle-input typetext defaultValue{title} {...register(title, { required: Title is required })} / /td td select idcategory-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 typesubmitSave/button button onClick{() setId(undefined)}Cancel/button /td /tr tr key{edit-${id}-mde} td colSpan{getAllColumns().length} Controller defaultValue{content} control{control} namecontent rules{{ required: Content is required }} render{({ field: { onChange, value } }) ( MDEditor value{value} onChange{onChange}>const { mutate } useDeleteManyIPost(); 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 } useManyICategory({ resource: categories, ids: categoryIds, queryOptions: { enabled: categoryIds.length 0 }, }); // 通过 setOptions 注入 table 的 meta供列 cell 读取 setOptions((prev) ({ ...prev, meta: { ...prev.meta, categoriesData }, })); // 2) 用 useSelect 获取完整分类选项用于筛选下拉与编辑行下拉 const { options } useSelectICategory({ 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 idtitle typetext value{(titleColumn?.getFilterValue() as string) ?? } onChange{(event) titleColumn?.setFilterValue(event.target.value)} / select idcategory aria-labelCategory 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.mode与sorters.mode设为client即可。展开行Markdown 内容预览子组件通过行选择列中的row.toggleExpanded()开关点击/可以展开/收起内容预览行。展开后渲染renderRowSubComponent用MDEditor.Markdown渲染文章的content字段const renderRowSubComponent useCallback( ({ row }: { row: RowIPost }) ( div>div classNamepagination button typebutton onClick{() setPageIndex(0)} disabled{!getCanPreviousPage()}{}/button button typebutton onClick{() previousPage()} disabled{!getCanPreviousPage()}{}/button button typebutton onClick{() nextPage()} disabled{!getCanNextPage()}{}/button button typebutton onClick{() setPageIndex(getPageCount() - 1)} disabled{!getCanNextPage()}{}/button span Page strong{getState().pagination.pageIndex 1} of {getPageCount()}/strong{ } /span span | Go to page:{ } input typenumber 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 核心的分页状态currentPage、pageSize、pageCount映射进useReactTable的initialState.pagination并设置manualPagination: true与pageCount让分页完全由 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行内编辑useFormsetId切换编辑行 受控表单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 同步并把编辑表单替换为useDrawerForm或useModalForm等更复杂的交互形态。所有能力都建立在 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),仅供参考
上一篇/下一篇内容由系统自动关联 返回资讯列表 →