Refine 中的 useNotification Hook 实战指南:open / close 与可撤销(Undoable)通知完整解析
【免费下载链接】refineA React Framework for building internal tools, admin panels, dashboards & B2B apps with unmatched flexibility.项目地址: https://gitcode.com/GitHub_Trending/re/refine
导读
useNotification是 Refine 框架中用于在应用任意位置弹出或关闭通知(Toast / Snackbar)的核心 Hook,它本身不关心 UI 实现,而是将open与close两个方法透传给你在<Refine>组件上配置的notificationProvider。本文以 basic-usage-live-preview.md 为主线,结合 index.md 与 notification-provider/index.md,先讲透useNotification的三种通知类型与可撤销流程,再下沉到源码层面解析其 Context 实现、OpenNotificationParams类型定义与内置 Provider 的落地方式,最后手把手带你从零实现一个基于react-toastify的自定义notificationProvider。读完本文,你将掌握在 Refine 项目中声明式地触发、更新、关闭通知,并实现"进度 + 撤销"交互的完整能力。
一、useNotification 是什么:一个只做透传的 Hook
useNotification的定位非常纯粹:在任何组件中获取open与close方法,用来随时打开或关闭通知。它本身不包含任何 UI 逻辑,而是从notificationProvider中取出这两个方法并原样返回。
这一点可以从源码得到直接印证。完整的实现只有 10 行:
// packages/core/src/hooks/notification/useNotification/index.ts import { useContext } from "react"; import { NotificationContext } from "@contexts/notification"; import type { INotificationContext } from "../../../contexts/notification/types"; export const useNotification = (): INotificationContext => { const { open, close } = useContext(NotificationContext); return { open, close }; };它通过useContext读取NotificationContext。这个 Context 由NotificationContextProvider提供,默认值为空对象{}:
// packages/core/src/contexts/notification/index.tsx export const NotificationContext = createContext<INotificationContext>({});而INotificationContext的类型定义(packages/core/src/contexts/notification/types.ts)同时给出了useNotification的完整返回签名:
export interface INotificationContext { open?: (params: OpenNotificationParams) => void; close?: (key: string) => void; } export type NotificationProvider = Required<INotificationContext>;两个方法都是可选的(?),因此文档中的示例一律使用open?.(...)、close?.(...)这种可选调用写法,即便项目没有配置任何通知 Provider 也不会抛错。
open / close 方法签名速查
| 属性 | 用途 | 类型 |
|---|---|---|
| open | 打开(或更新)一条通知 | (params: OpenNotificationParams) => void |
| close | 按key关闭一条通知 | (key: string) => void |
二、OpenNotificationParams:通知参数逐个拆解
open方法接收的唯一参数是OpenNotificationParams,其类型同样定义在 packages/core/src/contexts/notification/types.ts:
export type OpenNotificationParams = { key?: string; message: string; type: "success" | "error" | "progress"; description?: string; cancelMutation?: () => void; undoableTimeout?: number; };各字段的含义与使用要点如下:
| 字段 | 类型 | 是否必填 | 说明 |
|---|---|---|---|
key | string | 可选 | 通知的唯一标识。必须传入,因为close方法正是靠它来关闭指定通知;同一个key再次open会更新已有通知而非新建 |
message | string | 必填 | 通知主标题/主要内容 |
type | "success" \| "error" \| "progress" | 必填 | 通知类型。前两者渲染普通提示;progress用于可撤销(undoable)通知 |
description | string | 可选 | 通知的补充描述(次要文本) |
cancelMutation | () => void | 可选 | 撤销回调。用户点击 Undo 时执行,通常用于取消正在进行的数据变更 |
undoableTimeout | number | 可选 | 可撤销通知的倒计时秒数,配合type: "progress"使用 |
值得强调的是key的语义:不传key时无法通过close关闭这条通知;而传入相同key再调open,则会在既有通知上执行更新(更新文案、进度等),而不是叠加弹出新通知。这在倒计时刷新进度、以及多次操作同一条数据时非常有用。
三、基础用法:三种通知的触发与关闭
下面这段代码来自 basic-usage-live-preview.md 的在线可运行示例(MUI 实现,包裹在RefineMuiDemo中),它一次性演示了success、error、progress三种通知的触发:
import { useNotification } from "@refinedev/core"; import { Button, Stack } from "@mui/material"; const ExamplePage: React.FC = () => { const { open, close } = useNotification(); return ( <Stack spacing={2} direction="row"> <Button color="success" variant="outlined" size="small" onClick={() => open?.({ type: "success", message: "Success", description: "Success description", }) } > Success </Button> <Button color="error" variant="outlined" size="small" onClick={() => open?.({ type: "error", message: "Error", description: "Error description", }) } > Error </Button> <Button color="secondary" variant="outlined" size="small" onClick={() => open?.({ type: "progress", message: "Progress", undoableTimeout: 5, cancelMutation: () => { alert("cancelMutation"); }, }) } > Progress </Button> </Stack> ); };打开普通通知
const { open } = useNotification(); open?.({ type: "success", message: "Success", description: "This is a success message", });success/error两种类型对应普通提示,description作为补充说明展示在message之下;- 具体视觉样式完全取决于你接入的 UI 库(Ant Design、MUI、Mantine、Chakra UI 或自定义 Provider)。
按 key 关闭通知
const { close } = useNotification(); close?.("notification-key");要能成功关闭,前提是open时传入了同一个key:
open?.({ key: "notification-key", type: "success", message: "Success", });完整参数说明可参考 接口参考文档 中的
Open Notification Params一节(当前仓库文档树中对应documentation/docs/core/目录)。
四、可撤销(Undoable)通知:type 与倒计时机制
FAQ 中明确给出了可撤销通知的正确姿势:必须使用type: "progress",同时传入undoableTimeout(倒计时秒数)与cancelMutation(撤销回调):
const { open } = useNotification(); open?.({ type: "progress", message: "Progress", undoableTimeout: 5, cancelMutation: () => { // 当用户点击 Undo 按钮时,执行此回调 }, });其运行机制可以概括为三点(依据 notification-provider/index.md):
- 每秒递减:
undoableTimeout从设定值开始,每 1 秒减 1,直到 0; - 重复回调更新:倒计时期间,
open方法会携带相同的key被反复调用,因此 Provider 必须用新值更新已有通知的进度条,而不是不断新建通知; - 归零自动关闭:倒计时归零后通知自动关闭,此时数据变更正式生效;用户若在倒计时内点击 Undo,则执行
cancelMutation撤销变更。
内置 Provider 中的倒计时实现
以 MUI 内置 Provider 为例,packages/mui/src/providers/notificationProvider/index.tsx 中type === "progress"的分支会:
- 渲染
CircularDeterminate圆形进度组件,传入undoableTimeout与message; - 通过
enqueueSnackbar的autoHideDuration: (undoableTimeout ?? 0) * 1000设置自动关闭时长; - 在 Snackbar 的
action中放置 Undo 按钮,点击后依次执行cancelMutation?.()并closeSnackbar(key)关闭当前通知:
if (type === "progress") { const action = (key: any) => ( <IconButton onClick={() => { cancelMutation?.(); closeSnackbar(key); }} color="inherit" > <UndoOutlined /> </IconButton> ); enqueueSnackbar(<CircularDeterminate undoableTimeout={undoableTimeout ?? 0} message={message} />, { action, preventDuplicate: true, key, autoHideDuration: (undoableTimeout ?? 0) * 1000, }); }五、背后机制:notificationProvider 与默认实现
useNotification返回的open/close最终来自<Refine>组件上配置的notificationProvider。从 packages/core/src/components/containers/refine/index.tsx 的源码可以看到它的注入路径:notificationProvider被归一化为一个函数并求值,其结果通过NotificationContextProvider注入 Context:
const useNotificationProviderValues = React.useMemo(() => { return typeof notificationProvider === "function" ? notificationProvider : () => notificationProvider; }, [notificationProvider]); const notificationProviderContextValues = useNotificationProviderValues(); // ... <NotificationContextProvider {...notificationProviderContextValues}>notificationProvider本身是一个包含open与close两个方法的对象:
const notificationProvider = { open: () => {}, close: () => {}, };其类型定义(packages/core/src/contexts/notification/types.ts)与上面展示的OpenNotificationParams完全对应:
interface NotificationProvider { open: (params: OpenNotificationParams) => void; close: (key: string) => void; }默认行为:Refine 并不强制要求配置notificationProvider。未提供时,Context 默认值为空对象,应用可以在完全没有通知功能的情况下正常运行;只有当你的业务需要提示用户(尤其是配合undoable变更模式)时,才需要接入一个 Provider。
内置 Provider 一键接入
Refine 对主流 UI 库提供了开箱即用的useNotificationProvider:
Ant Design(@refinedev/antd):
import { useNotificationProvider } from "@refinedev/antd"; return ( <Refine //... notificationProvider={useNotificationProvider} /> );Material UI(@refinedev/mui,需包裹RefineSnackbarProvider):
import { useNotificationProvider, RefineSnackbarProvider } from "@refinedev/mui"; return ( <RefineSnackbarProvider> <Refine //... notificationProvider={useNotificationProvider} /> </RefineSnackbarProvider> );Mantine(@refinedev/mantine+@mantine/notifications的NotificationsProvider):
import { useNotificationProvider } from "@refinedev/mantine"; import { NotificationsProvider } from "@mantine/notifications"; return ( <NotificationsProvider position="top-right"> <Refine //... notificationProvider={useNotificationProvider} /> </NotificationsProvider> );Chakra UI(@refinedev/chakra-ui):
import { useNotificationProvider } from "@refinedev/chakra-ui"; return ( <Refine //... notificationProvider={useNotificationProvider()} /> );六、从零实现自定义 notificationProvider(react-toastify 实战)
如果你希望完全掌控通知的视觉与交互,可以参照下面的思路,基于react-toastify从零构建一个 Provider。核心逻辑是把open收到的参数映射到 toast 的创建/更新,把close映射到 toast 的销毁。仓库中完整的可运行实现见示例项目 examples/with-react-toastify。
1. 安装并挂载 ToastContainer
首先在App中引入ToastContainer与样式文件:
import { Refine } from "@refinedev/core"; import { ToastContainer } from "react-toastify"; import "react-toastify/dist/ReactToastify.css"; const App: React.FC = () => { return ( <Refine /* ... */> {/* ... */} <ToastContainer /> </Refine> ); }; export default App;2. 实现 open:创建与更新通知
基础版:把message展示为 toast,并用key作为toastId:
import { toast } from "react-toastify"; const notificationProvider: NotificationProvider = { open: ({ message, key, type }) => { toast(message, { toastId: key, type, }); }, };改进版:用toast.isActive(key)判断同key通知是否仍存在——存在则更新而非新建:
const notificationProvider: NotificationProvider = { open: ({ message, key, type }) => { if (toast.isActive(key)) { toast.update(key, { render: message, type, }); } else { toast(message, { toastId: key, type, }); } }, };3. 支持 progress:可撤销通知的完整实现
当变更模式为undoable时,Refine 会发送type: "progress",同时携带cancelMutation与undoableTimeout。如第四节所述,倒计时每秒触发一次open(同key),因此这里必须区分"首次创建"与"持续更新":
const notificationProvider: NotificationProvider = { open: ({ message, key, type }) => { if (type === "progress") { if (toast.isActive(key)) { toast.update(key, { progress: undoableTimeout && (undoableTimeout / 10) * 2, render: ( <UndoableNotification message={message} cancelMutation={cancelMutation} /> ), type: "default", }); } else { toast( <UndoableNotification message={message} cancelMutation={cancelMutation} />, { toastId: key, updateId: key, closeOnClick: false, closeButton: false, autoClose: false, progress: undoableTimeout && (undoableTimeout / 10) * 2, }, ); } } else { if (toast.isActive(key)) { toast.update(key, { render: message, closeButton: true, autoClose: 5000, type, }); } else { toast(message, { toastId: key, type, }); } } }, };对应的UndoableNotification组件负责渲染"消息 + Undo 按钮",点击后先执行cancelMutation再关闭 toast:
type UndoableNotification = { message: string; cancelMutation?: () => void; closeToast?: () => void; }; export const UndoableNotification: React.FC<UndoableNotification> = ({ closeToast, cancelMutation, message, }) => { return ( <div> <p>{message}</p> <button onClick={() => { cancelMutation?.(); closeToast?.(); }} > Undo </button> </div> ); };注意:
progress通知默认不可手动关闭(closeOnClick: false、closeButton: false),以保证用户在倒计时结束前始终有机会点击 Undo;普通通知则显式加上closeButton: true与autoClose: 5000,让用户可以在进度结束后自行关闭。
4. 实现 close:按 key 销毁
close的职责非常单一:接收通知的key并销毁对应 toast:
const notificationProvider: NotificationProvider = { //... close: (key) => toast.dismiss(key), };5. 在组件中消费
Provider 实现完毕后,即可通过useNotification在任意组件中使用open与close:
import { useNotification } from "@refinedev/core"; const { open } = useNotification(); open?.({ type: "success", message: "Hey", description: "I <3 Refine", key: "unique-id", });const { close } = useNotification(); close?.("displayed-notification-key");七、与数据变更的联动:useHandleNotification
在 Refine 的实践中,通知并不只靠手动调用。open还承担着数据变更结果反馈的职责:create、update、delete等 mutation 成功或失败时,会自动触发通知。这一联动由 packages/core/src/hooks/notification/useHandleNotification/index.ts 中的useHandleNotification完成:
import { useCallback } from "react"; import { useNotification } from "@hooks"; import type { OpenNotificationParams } from "../../../contexts/notification/types"; export const useHandleNotification = (): typeof handleNotification => { const { open } = useNotification(); const handleNotification = useCallback( (notification: OpenNotificationParams | false | undefined, fallbackNotification?: OpenNotificationParams) => { if (notification !== false) { if (notification) { open?.(notification); } else if (fallbackNotification) { open?.(fallbackNotification); } } }, [], ); return handleNotification; };对应地,OpenNotificationParams派生出了SuccessErrorNotification类型(packages/core/src/contexts/notification/types.ts),mutation 相关的 hook 通过successNotification/errorNotification属性接受"通知参数、false(静默不提示)或返回这两者的函数":
export type SuccessErrorNotification<TData = unknown, TError = unknown, TVariables = unknown> = { successNotification?: | OpenNotificationParams | false | ((data?: TData, values?: TVariables, resource?: string) => OpenNotificationParams | false | undefined); errorNotification?: | OpenNotificationParams | false | ((error?: TError, values?: TVariables, resource?: string) => OpenNotificationParams | false | undefined); };场景速览:把 useNotification 用进真实表单/表格
- 保存表单成功提示:
useForm的successNotification配置为{ type: "success", message: "保存成功" },提交成功即自动弹出; - 删除可撤销:配合
mutationMode="undoable"(参见 advanced-tutorials/mutation-mode.md),删除操作会先弹出一条progress通知并进入倒计时,用户可点击 Undo 取消删除; - 任意业务事件:在组件事件处理器中直接
open?.({ type: "error", message: "...", description: "..." }),无需关心底层 UI 库。
八、结语与延伸阅读
回顾全文,useNotification的完整链路可以概括为:<Refine>接收notificationProvider→ 经NotificationContextProvider注入 Context →useNotification通过useContext取出open/close→ 应用任意位置触发、更新、关闭通知。type: "progress"配合undoableTimeout与cancelMutation构成了完整的可撤销交互闭环,而内置的各 UI 库 Provider(antd / mui / mantine / chakra)与自定义 Provider 只是open/close的不同落地实现。
如果你需要继续深入,建议阅读以下仓库资源:
- notification-provider 完整文档:Provider 接口、内置实现与从零构建的完整代码
- useNotification 类型定义:
OpenNotificationParams、NotificationProvider、SuccessErrorNotification的权威出处 - MUI 内置 Provider 实现:查看 progress 通知与 Undo 按钮的完整渲染逻辑
- refine 容器组件接入点:
notificationProvider的注入与归一化 - useHandleNotification 实现:mutation 结果自动通知的调用链
- with-react-toastify 示例:完整可运行的自定义 Provider 工程
【免费下载链接】refineA React Framework for building internal tools, admin panels, dashboards & B2B apps with unmatched flexibility.项目地址: https://gitcode.com/GitHub_Trending/re/refine
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考