Refine v5 的 shadcn/ui DeleteButton 组件:安全删除、二次确认与 dataProvider 集成实战
2026/9/13 6:30:42 网站建设 项目流程

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 的ButtonPopover实现,点击后弹出二次确认气泡,确认后调用dataProviderdeleteOne方法完成数据删除。本文以 Delete Button 官方文档 为核心,结合 refine-ui 注册表源码、核心 useDelete hook 与 ui-tests 测试用例,带你掌握从安装、表格内嵌使用、全部 Props 配置到源码级原理的完整实战链路。

组件概览:确认式删除的两种表现形态

<DeleteButton>的交互链路非常清晰:

  1. 渲染一个variant="destructive"(危险样式)的按钮,默认包含 Trash 垃圾桶图标与 "Delete" 文案;
  2. 点击按钮后弹出Popover气泡,展示确认标题与「确认 / 取消」两个操作按钮;
  3. 点击确认后,组件调用核心 hookuseDelete,最终执行dataProvider提供的deleteOne方法;
  4. 删除成功后自动清理相关查询缓存、触发列表刷新,并弹出成功通知。

注意:旧版 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:buttonpopover,即安装时会自动补齐 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 源码 中,resourceid会一并交给useResourceParams做解析,得到最终用于请求的resourceidentifierid

onSuccess

删除成功后的回调,接收deleteOne的返回值:

const MyComponent = () => { return ( <DeleteButton recordItemId="123" onSuccess={(value) => { console.log("Record deleted successfully", value); }} /> ); };

在底层,useDeleteButton 的 onConfirm 将onSuccess作为useDelete返回的mutateonSuccess选项传入,因此它总是在数据真正删除成功之后才触发。

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/confirmOkLabeltranslate("buttons.delete", "Delete")
  • confirmTitletranslate("buttons.confirm", "Are you sure?")
  • cancelLabeltranslate("buttons.cancel", "Cancel")

这意味着只要你的 i18n 字典中配置了buttons.deletebuttons.confirmbuttons.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 源码 中,successNotificationerrorNotification既可以是静态配置,也可以是接收响应/错误参数的函数(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(variantsizeclassNameonClickdisabled等)都可直接使用。组件内部通过{...rest}展开到内层<Button>上,其中disabled与权限/加载状态合并(isDisabled = disabled || rest.disabled || loading)。

完整 Props 速查表

PropertyTypeDefaultDescription
recordItemIdBaseKey(string or number)Inferred from route params要删除的记录 ID
resourcestringInferred from route资源名或标识符
onSuccess(value: any) => void-删除成功后的回调
confirmTitlestring"Are you sure?"确认气泡标题
confirmOkTextstring"Delete"确认按钮文案
confirmCancelTextstring"Cancel"取消按钮文案
successNotificationstring \| false \| objectDefault message成功通知,false禁用
errorNotificationstring \| false \| objectDefault message失败通知,false禁用
metaRecord<string, unknown>-传给delete方法的额外元数据
hideTextbooleanfalsetrue时仅显示图标
accessControl{ enabled?: boolean; hideIfUnauthorized?: boolean }{ enabled: true, hideIfUnauthorized: false }权限控制配置
childrenReactNodeDefault text & icon按钮自定义内容
...restReact.ComponentProps<typeof Button>-透传给 shadcn/ui Button(variantsizeclassNameonClick等)

源码级原理:从点击确认到数据删除的完整调用链

组件之所以能自动完成「推断资源 → 权限校验 → 弹窗确认 → 调用 API → 缓存失效 → 通知提示」全流程,核心在于两层封装:

第一层:useDeleteButton(按钮逻辑层)

useDeleteButton 是 Refine 核心包暴露给所有 UI 框架的通用 hook,返回按钮所需的全部状态与方法:

  • 通过useDelete()拿到mutateisPendingvariables
  • 通过useResourceParams推断idresourceidentifier
  • 通过useButtonCanAccess完成deleteaction 的权限判定,得到hiddendisabledtitle
  • 通过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还会removeQueriesone(详情)缓存;
  • 失败回滚onError会把context.previousQueries中的旧缓存全部写回,保证失败后 UI 恢复到删除前状态;
  • 实时发布与审计:成功后通过publishresources/{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.deletebuttons.confirmbuttons.cancelnotifications.deleteSuccessnotifications.deleteError等翻译键决定了所有默认文案,多语言项目只需补字典。
  • 配置accessControlsuccessNotification:在权限敏感的资源上开启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),仅供参考

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询