Refine v5 的 shadcn/ui DeleteButton 组件:安全删除、二次确认与 dataProvider 集成实战
【免费下载链接】refineA React Framework for building internal tools, admin panels, dashboards & B2B apps with unmatched flexibility.项目地址: https://gitcode.com/GitHub_Trending/re/refine
<DeleteButton>是 Refine v5 中用于删除操作的开箱即用按钮组件,它基于 shadcn/ui 的Button与Popover实现,点击后弹出二次确认气泡,确认后调用dataProvider的deleteOne方法完成数据删除。本文以 Delete Button 官方文档 为核心,结合 refine-ui 注册表源码、核心 useDelete hook 与 ui-tests 测试用例,带你掌握从安装、表格内嵌使用、全部 Props 配置到源码级原理的完整实战链路。
组件概览:确认式删除的两种表现形态
<DeleteButton>的交互链路非常清晰:
- 渲染一个
variant="destructive"(危险样式)的按钮,默认包含 Trash 垃圾桶图标与 "Delete" 文案; - 点击按钮后弹出
Popover气泡,展示确认标题与「确认 / 取消」两个操作按钮; - 点击确认后,组件调用核心 hook
useDelete,最终执行dataProvider提供的deleteOne方法; - 删除成功后自动清理相关查询缓存、触发列表刷新,并弹出成功通知。
注意:旧版 Refine(v4 及更早)的删除按钮基于 antd 的
Popconfirm实现;本文描述的DeleteButton是基于 shadcn/uiPopover的 v5 版本,二者的 Props 与交互略有差异。
安装:通过 shadcn CLI 一键注册
npx shadcn@latest add https://ui.refine.dev/r/buttons.json该命令会通过 shadcn 的 registry 协议下载 buttons.json 注册表清单,一次性安装全部按钮组件(create、edit、delete、show、clone、list、refresh),组件默认输出到项目的src/components/refine-ui/buttons/目录。
从注册表清单可以看到,buttons注册项的依赖如下:
dependencies:@refinedev/core(核心逻辑所在)与lucide-react(Trash 等图标);registryDependencies:button与popover,即安装时会自动补齐 shadcn/ui 的 Button 与 Popover 基础组件;files: 每个按钮对应一个registry/new-york/refine-ui/buttons/*.tsx源文件,其中delete.tsx会被写入到src/components/refine-ui/buttons/delete.tsx。
因此安装完成后,你在代码中通过@/components/refine-ui/buttons/delete导入组件。如果你只想使用删除按钮,也可以只复制delete.tsx的源码与相关依赖。
基础用法:在列表表格中为每一行提供删除操作
最常见的场景是在数据列表的每一行末尾放置一个删除按钮。以下示例来自官方文档,在 antd/自建 Table 中为每个帖子渲染删除操作:
import { DeleteButton } from "@/components/refine-ui/buttons/delete"; import { Table, TableBody, TableCell, TableRow } from "@/components/ui/table"; const PostList = () => { const posts = [ { id: 1, title: "First Post" }, { id: 2, title: "Second Post" }, ]; return ( <Table> <TableBody> {posts.map((post) => ( <TableRow key={post.id}> <TableCell>{post.title}</TableCell> <TableCell> <DeleteButton resource="posts" recordItemId={post.id} /> </TableCell> </TableRow> ))} </TableBody> </Table> ); };这里的resource告诉组件数据属于哪个资源,recordItemId指定要删除的具体记录 ID。若二者缺省,组件会从当前路由中自动推断(详见下文 Props 说明)。
Props 详解:从目标记录到通知反馈的完整配置
官方文档共给出 10 个核心 Props,下面逐一说明其作用、默认值与典型用法。
recordItemId
指定将要删除的记录 ID,类型为BaseKey(string 或 number)。默认情况下,组件会从路由参数(URL 中的:id)中自动读取,因此在使用详情页(show/edit 页面)时可以不传:
import { DeleteButton } from "@/components/refine-ui/buttons/delete"; const MyComponent = () => { return <DeleteButton resource="posts" recordItemId="123" />; };resource
指定记录所属的资源名,默认从当前路由推断。若你正在操作的资源与当前路由不一致(例如在分类页面删除文章),就需要显式传入。也可以传入资源定义的identifier来替代name:
import { DeleteButton } from "@/components/refine-ui/buttons/delete"; const MyComponent = () => { return <DeleteButton resource="categories" recordItemId="123" />; };在 useDeleteButton 源码 中,resource与id会一并交给useResourceParams做解析,得到最终用于请求的resource、identifier与id。
onSuccess
删除成功后的回调,接收deleteOne的返回值:
const MyComponent = () => { return ( <DeleteButton recordItemId="123" onSuccess={(value) => { console.log("Record deleted successfully", value); }} /> ); };在底层,useDeleteButton 的 onConfirm 将onSuccess作为useDelete返回的mutate的onSuccess选项传入,因此它总是在数据真正删除成功之后才触发。
confirmTitle / confirmOkText / confirmCancelText
这三个 Props 用于定制确认气泡的文案,分别对应标题、确认按钮、取消按钮:
<DeleteButton recordItemId="123" confirmTitle="Delete this post?" confirmOkText="Yes, delete it" confirmCancelText="No, keep it" />默认值分别为"Are you sure?"、"Delete"与"Cancel"。值得注意的是,这三个默认值并非硬编码在组件里,而是通过 i18n 翻译函数生成(见 useDeleteButton 源码):
label/confirmOkLabel→translate("buttons.delete", "Delete")confirmTitle→translate("buttons.confirm", "Are you sure?")cancelLabel→translate("buttons.cancel", "Cancel")
这意味着只要你的 i18n 字典中配置了buttons.delete、buttons.confirm、buttons.cancel,无需修改代码即可实现多语言文案。
meta
向dataProvider.deleteOne方法传递额外的元数据参数(如 GraphQL 查询字段、权限上下文等):
const MyComponent = () => { return <DeleteButton meta={{ authorId: "10" }} />; };在 useDelete 实现 中,传入的meta会与全局getMeta合并为combinedMeta,最终作为deleteOne({ resource, id, meta, variables })的一部分发出。meta也用于构建 React Query 的缓存 key,因此不同meta的删除操作不会互相污染缓存。
hideText
设为true时仅显示图标,隐藏文字:
<DeleteButton recordItemId="123" hideText={true} />在 delete.tsx 组件源码 中,默认渲染逻辑是「Trash 图标 + label 文字」的组合;传入children或通过hideText由父级控制时,文字部分会被替换或隐藏。UI 测试也专门验证了该行为(见 ui-tests 的 hideText 用例)。
accessControl
控制按钮的权限校验行为,仅在向<Refine/>提供了accessControlProvider时生效:
<DeleteButton accessControl={{ enabled: true, hideIfUnauthorized: true, }} />两个子属性的默认值为{ enabled: true, hideIfUnauthorized: false },含义如下:
enabled: false→ 跳过权限检查,按钮始终可用;hideIfUnauthorized: true→ 用户无delete权限时直接不渲染按钮(返回null);hideIfUnauthorized: false(默认)→ 无权限时按钮渲染为禁用态,并携带title提示原因。
组件源码 delete.tsx 中if (isHidden) return null;即为隐藏逻辑。权限的解析由useButtonCanAccess(action 为"delete")完成(见 useDeleteButton 源码)。
ui-tests 对权限场景覆盖得非常细致(见 delete.tsx 测试):无权限时按钮被禁用且title显示 "Access Denied"、hideIfUnauthorized时按钮不渲染、accessControl={{ enabled: false }}可以绕过全局enableAccessControl: true的配置等。
successNotification / errorNotification
定制删除成功 / 失败时的通知内容,类型为string | false | object。传入false可直接禁用通知:
<DeleteButton recordItemId="123" successNotification={{ message: "Post deleted successfully!", description: "The post has been removed from your blog.", type: "success", }} /><DeleteButton recordItemId="123" successNotification={false} />失败通知同样支持定制或禁用:
<DeleteButton recordItemId="123" errorNotification={{ message: "Failed to delete post", description: "Please try again later.", type: "error", }} />在 useDelete 源码 中,successNotification与errorNotification既可以是静态配置,也可以是接收响应/错误参数的函数(typeof successNotification === "function"分支);最终通过handleNotification统一派发。默认通知文案也来自 i18n:成功为notifications.deleteSuccess,失败为notifications.deleteError(包含statusCode)。
children
用来自定义按钮内容,替换默认的「图标 + Delete」:
<DeleteButton recordItemId="123">Remove</DeleteButton>...rest(透传 Props)
DeleteButton的类型定义继承自React.ComponentProps<typeof Button>(见 delete.tsx 源码),因此 shadcn/ui Button 的全部 Props(variant、size、className、onClick、disabled等)都可直接使用。组件内部通过{...rest}展开到内层<Button>上,其中disabled与权限/加载状态合并(isDisabled = disabled || rest.disabled || loading)。
完整 Props 速查表
| Property | Type | Default | Description |
|---|---|---|---|
recordItemId | BaseKey(string or number) | Inferred from route params | 要删除的记录 ID |
resource | string | Inferred from route | 资源名或标识符 |
onSuccess | (value: any) => void | - | 删除成功后的回调 |
confirmTitle | string | "Are you sure?" | 确认气泡标题 |
confirmOkText | string | "Delete" | 确认按钮文案 |
confirmCancelText | string | "Cancel" | 取消按钮文案 |
successNotification | string \| false \| object | Default message | 成功通知,false禁用 |
errorNotification | string \| false \| object | Default message | 失败通知,false禁用 |
meta | Record<string, unknown> | - | 传给delete方法的额外元数据 |
hideText | boolean | false | 为true时仅显示图标 |
accessControl | { enabled?: boolean; hideIfUnauthorized?: boolean } | { enabled: true, hideIfUnauthorized: false } | 权限控制配置 |
children | ReactNode | Default text & icon | 按钮自定义内容 |
...rest | React.ComponentProps<typeof Button> | - | 透传给 shadcn/ui Button(variant、size、className、onClick等) |
源码级原理:从点击确认到数据删除的完整调用链
组件之所以能自动完成「推断资源 → 权限校验 → 弹窗确认 → 调用 API → 缓存失效 → 通知提示」全流程,核心在于两层封装:
第一层:useDeleteButton(按钮逻辑层)
useDeleteButton 是 Refine 核心包暴露给所有 UI 框架的通用 hook,返回按钮所需的全部状态与方法:
- 通过
useDelete()拿到mutate、isPending、variables; - 通过
useResourceParams推断id、resource、identifier; - 通过
useButtonCanAccess完成deleteaction 的权限判定,得到hidden、disabled、title; - 通过
useMutationMode读取全局 mutation 模式(pessimistic / optimistic / undoable); onConfirm内部会先调用setWarnWhen(false)(关闭表单「未保存更改」警告),再以{ id, resource, mutationMode, successNotification, errorNotification, meta, dataProviderName, invalidates }调用mutate。
组件层的loading = id === variables?.id && isPending判断保证:只有正在删除「当前按钮对应记录」时,该按钮才会显示加载态(Loader2 旋转图标)。
第二层:useDelete(数据层)
useDelete 本质上是 TanStack QueryuseMutation的删除专用封装,其mutationFn会调用dataProvider.deleteOne。值得注意的细节:
- mutation 模式:
mutationMode === "undoable"时不会立即调用 API,而是通过notificationDispatch加入可撤销队列,倒计时结束后才真正执行deleteOne; - 乐观更新:在
pessimistic之外的模式下,onMutate会先从缓存中过滤掉被删除记录(data.filter(...)并让total - 1),实现列表即时刷新; - 缓存失效:
onSettled默认invalidates: ["list", "many"],即删除后自动使 list 与 many 类查询失效;onSuccess还会removeQueries掉one(详情)缓存; - 失败回滚:
onError会把context.previousQueries中的旧缓存全部写回,保证失败后 UI 恢复到删除前状态; - 实时发布与审计:成功后通过
publish向resources/{resource}频道广播deleted事件(配合 live provider 实现多端实时同步),并通过log.mutate记录action: "delete"的审计日志; - 错误通知:
err.message !== "mutationCancelled"时才会展示错误通知,撤销操作不会误报。
组件层:Popover 与 Button 的组装
delete.tsx 将上述逻辑渲染为 UI:
- 外层
Popover受控于openstate,PopoverTrigger包裹一个<span>(避免 button 嵌套导致的非法 DOM); - 内层
Button使用variant="destructive",加载时前置Loader2旋转图标; PopoverContent中,取消按钮为variant="outline"并setOpen(false);确认按钮为variant="destructive",点击后执行onConfirm()并关闭气泡;- 确认按钮在
loading时禁用,防止重复提交。
测试验证:ui-tests 如何守护删除按钮行为
仓库中的 按钮通用测试集 通过buttonDeleteTests工厂函数,对任意 UI 框架实现的 DeleteButton 统一执行断言,覆盖了:
- 默认渲染:按钮可点击,文案为 "Delete"(
getByText("Delete").closest("button")不为禁用态); - 禁用/隐藏:
disabled时点击不触发任何回调,hidden时按钮从 DOM 移除; - 自定义文案:
children替换默认文本; - 权限控制矩阵:全局配置与
accessControl属性的各种组合(禁用/隐藏/绕过); - 确认弹窗:点击后出现 "Are you sure?" 与 "Cancel",确认后调用
deleteOne一次(expect(deleteOneMock).toHaveBeenCalledTimes(1)); - 记录 ID 传递:
deleteOne收到的参数包含{ id: "record-id" }; onSuccess回调:确认删除后onSuccessMock被调用一次;- 自定义确认文案与
mutationMode="pessimistic"、自定义resource等场景。
这套测试与 antd、MUI、Chakra UI、Mantine 等各框架的按钮实现共享,保证了<DeleteButton>在所有 UI 集成中行为一致——这是你在自己项目中放心使用它的重要保障。
小结与最佳实践
- 始终显式传入
recordItemId:在列表页中,路由通常没有:id,务必为每一行传入记录 ID;只有详情/编辑页才可依赖路由推断。 - 删除是危险操作,务必保留二次确认:
<DeleteButton>默认内置确认气泡,不要通过自定义children或透传onClick绕过它。 - 用
confirmTitle/confirmOkText明确语义:将 "Delete" 改为 "Yes, delete it" 这类明确文案,可显著降低误删概率。 - 善用 i18n 键:
buttons.delete、buttons.confirm、buttons.cancel、notifications.deleteSuccess、notifications.deleteError等翻译键决定了所有默认文案,多语言项目只需补字典。 - 配置
accessControl与successNotification:在权限敏感的资源上开启hideIfUnauthorized: true,并通过定制通知把删除结果明确反馈给用户。 - 结合 mutation 模式使用:默认的悲观模式最安全;若追求列表即时响应,可在
<Refine/>或按钮层面开启乐观/可撤销模式,删除按钮会自动适配(undoable 模式下会显示撤销倒计时)。
如需继续深入,可进一步阅读:dataProvider 接口、useDelete hook 文档、权限控制文档,以及仓库内的 delete.tsx 实现、useDeleteButton 源码、useDelete 源码 与 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),仅供参考