Refine v5 批量删除实战:使用 useDeleteMany 在 Ant Design 表格中实现多选删除
【免费下载链接】refineA React Framework for building internal tools, admin panels, dashboards & B2B apps with unmatched flexibility.项目地址: https://gitcode.com/GitHub_Trending/re/refine
useDeleteMany是 Refine 数据层(data hooks)中用于一次性删除多条记录的核心 Hook,本文以仓库中的table-antd-use-delete-many官方示例为主体,完整讲解如何在 Ant Design 表格上实现"勾选多行 → 点击删除"的批量删除交互,并深入packages/core源码剖析其底层实现:从 dataProvider 的deleteMany调用、deleteOne降级逻辑,到乐观更新、查询失效(invalidate)与实时通知机制。读完本文,你将掌握useDeleteMany的全部参数、返回值与最佳实践,并理解它在 Refine 数据流中扮演的角色。
示例场景:表格多选批量删除
原文档 useDeleteMany.md 描述的场景非常明确:useDeleteMany一次从数据库中删除多条数据,而在示例中,我们用它从表格里移除多条记录——用户通过勾选行选中记录,点击删除按钮即可批量移除。
对应示例源码位于 examples/table-antd-use-delete-many,应用使用@refinedev/simple-rest连接https://api.fake-rest.refine.dev这一演示 API(见 App.tsx),资源定义为posts,包含 list / create / edit / show 四条路由。核心页面是 src/pages/posts/list.tsx。
从零实现:勾选、删除、刷新
1. 初始化表格与分类数据
页面通过useTable<IPost>()获取 Ant Design 表格所需的tableProps,同时用useMany拉取文章关联的categories,用于把category.id渲染成分类标题:
const { tableProps } = useTable<IPost>(); const categoryIds = tableProps?.dataSource?.map((item) => item.category.id) ?? []; const { result: data, query: { isLoading }, } = useMany<ICategory>({ resource: "categories", ids: categoryIds, queryOptions: { enabled: categoryIds.length > 0, }, });注意这里的queryOptions.enabled:当表格还没有数据时categoryIds为空数组,此时禁用该查询,避免发无意义的请求。
2. 调用 useDeleteMany 并执行删除
这是整页的核心:
const { mutate, mutation: { isPending: deleteManyIsLoading }, } = useDeleteMany<IPost>(); const deleteSelectedItems = () => { mutate( { resource: "posts", ids: selectedRowKeys.map(String), }, { onSuccess: () => { setSelectedRowKeys([]); }, }, ); };要点解析:
mutate是触发删除的函数,必填参数resource(资源名,对应 API 端点路径)与ids(要删除的记录主键数组)。selectedRowKeys来自 Ant Design 的rowSelection状态,map(String)确保主键统一为字符串形式传给 Hook。- 从
mutation.isPending中解构出的deleteManyIsLoading用来驱动按钮的loading态。 onSuccess回调在删除成功后清空选中状态——这正是原文档强调的:useDeleteMany的mutationOptions不支持onSuccess/onError(它们会被内置实现覆盖),需要这类回调时请像这里一样作为mutate的第二个参数传入。
3. 接入表格多选
Ant Design 的Table通过rowSelection开启行勾选:
const [selectedRowKeys, setSelectedRowKeys] = React.useState<React.Key[]>([]); const onSelectChange = (selectedRowKeys: React.Key[]) => { setSelectedRowKeys(selectedRowKeys); }; const rowSelection = { selectedRowKeys, onChange: onSelectChange, selections: [ Table.SELECTION_ALL, Table.SELECTION_INVERT, Table.SELECTION_NONE, ], }; const hasSelected = selectedRowKeys.length > 0; return ( <List headerProps={{ subTitle: ( <> <Button type="primary" onClick={deleteSelectedItems} disabled={!hasSelected} loading={deleteManyIsLoading} > Delete Selected </Button> <span style={{ marginLeft: 8 }}> {hasSelected ? `Selected ${selectedRowKeys.length} items` : ""} </span> </> ), }} > <Table {...tableProps} rowSelection={rowSelection} rowKey="id"> {/* 列定义:ID / Title / Status / Category / Actions */} </Table> </List> );这里有三处值得注意的细节:
Table.SELECTION_ALL / SELECTION_INVERT / SELECTION_NONE让表头下拉菜单支持"全选 / 反选 / 取消全选"。disabled={!hasSelected}保证未选中任何行时按钮不可点,配合loading防止重复提交。List组件的headerProps.subTitle把操作按钮放进页面标题栏,是一种整洁的布局做法。
完整的列定义还包含操作列(编辑、查看按钮),位于 list.tsx,可作为参照。
批量删除的关键参数
原文档将useDeleteMany定位为 TanStack QueryuseMutation的扩展版:它继承useMutation的全部能力,并额外增加了 Refine 的数据层能力(通知、失效、实时发布等)。参考 use-delete-many 文档,常用参数如下:
| 参数 | 类型 | 默认值 | 说明 |
|---|---|---|---|
resource(必填) | string | — | 传给 dataProviderdeleteMany的资源名,通常即 API 端点路径 |
ids(必填) | BaseKey[] | — | 要删除的记录主键数组 |
mutationMode | "pessimistic" \| "optimistic" \| "undoable" | "pessimistic" | 决定变更在何时执行(详见下文) |
undoableTimeout | number | 5000(毫秒) | undoable模式下等待执行的时长 |
onCancel | (cancelMutation) => void | — | undoable模式下提供取消句柄;定义后系统不再自动弹撤销通知 |
successNotification/errorNotification | SuccessErrorNotification | 内置文案 | 自定义成功 / 失败通知 |
meta | MetaDataQuery | {} | 透传给 dataProvider 的附加信息(如自定义请求头、GraphQL 查询) |
dataProviderName | string | "default" | 存在多个 dataProvider 时指定使用哪一个 |
invalidates | "all" \| "resourceAll" \| "list" \| "many" \| "detail" \| false | ["list", "many"] | 变更完成后要失效并重取的查询 |
三种 mutationMode 的取舍
- pessimistic(默认):先请求后端,成功后再更新界面。最保守、数据最可靠。
- optimistic:立即在界面上移除记录,请求失败时回滚缓存。
- undoable:等待
undoableTimeout毫秒(默认 5 秒)后才真正执行,期间可撤销,适合误删保护场景。
这三种模式在源码测试 packages/core/src/hooks/data/useDeleteMany.spec.tsx 中均有对应用例验证(详见下文"测试与验证")。
多 dataProvider 与自定义通知示例
const { mutate, mutation } = useDeleteMany(); mutate({ resource: "products", ids: [1, 2, 3], dataProviderName: "second-data-provider", successNotification: (data, ids, resource) => ({ message: `${ids.length} products deleted.`, description: "Success with no errors", type: "success", }), errorNotification: (error, ids, resource) => ({ message: "Something went wrong", description: error.message, type: "error", }), });源码剖析:useDeleteMany 的完整调用链
useDeleteMany的实现位于 packages/core/src/hooks/data/useDeleteMany.ts,阅读它可以把"黑盒"变成"白盒"。
1. mutationFn:优先 deleteMany,降级 deleteOne
Hook 底层创建了一个 TanStack QueryuseMutation(useDeleteMany.ts),其mutationFn内部:
- 通过
useResourceParams的select解析resource,用useDataProvider按dataProviderName选出目标 provider; - 如果 provider 实现了
deleteMany,直接调用selectedDataProvider.deleteMany({ resource, ids, meta, variables }); - 如果 provider 没有
deleteMany,则退化为handleMultiple(ids.map((id) => deleteOne({ ... })))——即逐条调用deleteOne,对每个 id 各发一次请求。原文档明确提示:这是不推荐的做法,最好在 dataProvider 中实现deleteMany。
以@refinedev/rest为例,其deleteMany会按getEndpoint解析端点、buildHeaders组装请求头,并对params.resource发起DELETE请求(见 create-data-provider.ts)。
2. onMutate:乐观更新与缓存快照
当mutationMode !== "pessimistic"时,onMutate(useDeleteMany.ts)会:
- 用
queryClient.cancelQueries取消进行中的相关查询; - 用
getQueriesData保存当前缓存快照(previousQueries),供失败回滚; - 对
list、many、one三类查询做本地更新——从列表数据中过滤掉被删除的 ids,并把total减一。
这也解释了为什么示例删除后表格自动刷新:数据已经通过缓存层同步更新。
3. onSettled / onSuccess / onError:失效、通知与回滚
- onSettled(useDeleteMany.ts):默认调用
invalidateStore({ invalidates: ["list", "many"] })失效列表类查询(可用invalidates参数覆盖),并清理 undoable 队列中的通知条目。 - onSuccess:从缓存移除被删记录的
one查询;发出默认成功通知(文案由notifications.deleteSuccess翻译键控制);若有 Live Provider,则调用publish向resources/{resource}频道发布deleted事件(useDeleteMany.ts);同时通过useLog记录deleteMany审计日志。 - onError:用快照
previousQueries恢复被乐观更新的缓存,触发useOnError的全局错误处理,并弹出默认错误通知(除非错误为主动取消的mutationCancelled)。
4. undoable 模式实现
在mutationMode === "undoable"时,mutationFn 不会立刻请求后端,而是构造一个 Promise,通过notificationDispatch向 undoable 队列注册cancelMutation与doMutation,等待undoableTimeout秒(useDeleteMany.ts);倒计时结束才真正执行删除,期间用户可撤销。
测试与验证:三种模式与 E2E 行为
仓库同时提供了单元测试与端到端测试,可作为实现行为的权威佐证。
单元测试:pessimistic / optimistic / undoable
packages/core/src/hooks/data/useDeleteMany.spec.tsx 使用MockJSONServer与TestWrapper渲染 Hook:
- pessimistic:
mutate({ resource: "posts", ids: ["1"] })后等待mutation.isSuccess为真; - optimistic:mock 一个 1000ms 后 reject 的
deleteMany,先断言列表长度由 2 变为 0(乐观移除),请求失败后断言长度恢复为 2(回滚生效); - undoable:
mutationMode: "undoable", undoableTimeout: 1000下执行删除并最终成功。
E2E 测试:完整用户路径
cypress/e2e/table-antd-use-delete-many/all.cy.ts 用 Cypress 验证了真实页面行为:
- 点击表头复选框全选后,
.ant-table-row-selected应恰好有 10 行; - 未选中任何行时,主按钮(
.ant-btn-primary)应处于 disabled 状态; - 勾选两行后点击 "Delete" 按钮,应发出两次删除请求(
cy.wait("@deletePost")两次)。
运行示例
你可以通过 Refine CLI 在本地快速启动该示例:
npm create refine-app@latest -- --example table-antd-use-delete-many示例完整源码位于 examples/table-antd-use-delete-many,依赖@refinedev/core、@refinedev/antd与@refinedev/simple-rest;useDeleteMany的类型定义与实现可进一步查阅 packages/core/src/hooks/data/useDeleteMany.ts 及其配套文档 documentation/docs/data/hooks/use-delete-many/index.md。
小结
本文以官方table-antd-use-delete-many示例为主线,完整覆盖了"表格多选 + 批量删除"的落地路径:先用useTable+rowSelection收集选中主键,再交给useDeleteMany的mutate执行;随后从源码层厘清了 Hook 的完整生命周期——deleteMany优先、deleteOne降级、乐观更新、失败回滚、查询失效、通知与实时发布。掌握了这些,你便可以在自己的 Refine 应用中安全、高效地实现批量删除,并依据mutationMode在数据可靠性、交互即时性与误删保护之间做出合理权衡。
【免费下载链接】refineA React Framework for building internal tools, admin panels, dashboards & B2B apps with unmatched flexibility.项目地址: https://gitcode.com/GitHub_Trending/re/refine
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考