首页
/ React Router useBeforeUnload Hook 详解:在页面卸载前拦截并执行清理逻辑

React Router useBeforeUnload Hook 详解:在页面卸载前拦截并执行清理逻辑

2026-09-07 11:51:00作者:姚月梅Lane

React Router 提供的 useBeforeUnload Hook 让组件能够在浏览器窗口真正被关闭、刷新或跳离页面之前,声明式地注册一个回调函数,用于监听 Window 的 beforeunload 事件。它适用于框架模式(framework)、数据路由模式(data)与声明式路由模式(declarative)三种使用场景。读完本文,你将掌握 useBeforeUnload 的完整签名、参数语义与底层实现原理,并能正确区分它与 useBlocker / unstable_usePrompt 的职责边界,写出可靠的"离开前兜底"逻辑。

一、这个 Hook 解决什么问题

在 React 单页应用中,"离开页面"其实有两种截然不同的含义:

  1. 站内路由跳转:例如从 /dashboard 跳到 /settings,此时页面并没有被卸载,SPA 只是替换了视图;
  2. 真正的页面卸载:用户点击刷新、关闭标签页,或跳转到外部网站,此时整个文档会被销毁。

useBeforeUnload 只针对第二种场景。当用户在表单中填写了尚未保存的内容、正在上传文件、或需要在离开前向服务端发送最后的"心跳"时,都可以借助它获得一次执行兜底逻辑的机会。

与"手动往组件里塞 window.addEventListener"相比,它带来了 React 化的封装收益:生命周期由 useEffect 管理,组件卸载时自动移除监听器,避免了内存泄漏,也无需重复编写样板代码。

二、函数签名与参数详解

docs/api/hooks/useBeforeUnload.md 可知,该 Hook 的类型签名如下:

function useBeforeUnload(
  callback: (event: BeforeUnloadEvent) => any,
  options?: {
    capture?: boolean;
  },
): void
参数 类型 说明
callback (event: BeforeUnloadEvent) => any beforeunload 事件触发时被调用的回调函数。浏览器会把原生事件对象传入
options.capture boolean 若为 true,事件将在捕获阶段被处理。默认值为 false
返回值 void 无返回值

2.1 callback:卸载前执行的工作

callback 接收一个标准的 BeforeUnloadEvent,因此它既能读取事件的默认行为(例如调用 event.preventDefault() 来弹出浏览器的原生确认对话框),也能执行任意"临别前"的同步清理工作。

一个典型的场景是拦截未保存的编辑内容:

import { useBeforeUnload } from "react-router";

function Editor() {
  const [isDirty, setIsDirty] = React.useState(false);

  useBeforeUnload(
    React.useCallback(
      (event: BeforeUnloadEvent) => {
        if (isDirty) {
          // 触发浏览器原生的"离开此页面?"确认对话框
          event.preventDefault();
        }
      },
      [isDirty],
    ),
  );

  return <textarea onChange={() => setIsDirty(true)} />;
}

2.2 options.capture:控制监听阶段

capture 默认为 false,即采用冒泡阶段监听。只有在需要优先于页面内其它 beforeunload 监听器、先于它们做出反应时,才有必要设置 options={{ capture: true }}

useBeforeUnload(handler, { capture: true });

三、源码级实现:一条 useEffect 背后的原理

该文档由源码中的 JSDoc 注释自动生成,而真正实现位于 packages/react-router/lib/dom/lib.tsx。完整实现只有十几行,可以逐行拆解:

export function useBeforeUnload(
  callback: (event: BeforeUnloadEvent) => any,
  options?: { capture?: boolean },
): void {
  let { capture } = options || {};
  React.useEffect(() => {
    let opts = capture != null ? { capture } : undefined;
    window.addEventListener("beforeunload", callback, opts);
    return () => {
      window.removeEventListener("beforeunload", callback, opts);
    };
  }, [callback, capture]);
}

从实现中可以提炼出几个关键设计决策:

  • 单一副作用入口:所有监听/解除监听逻辑都收敛在一个 useEffect 中,effect 的 cleanup 函数负责在组件卸载或依赖变化时移除监听,保证无泄漏;
  • 依赖数组是 [callback, capture]:这意味着当传入的 callback 函数引用发生变化时,监听会被拆除并重新绑定。因此 callback 最好用 React.useCallback 包裹,否则每次渲染都会重新订阅事件,造成不必要的抖动;
  • capture 缺省即不传:实现中 capture != null 时才向 addEventListener 的第三个参数传入 { capture },否则传 undefined,等效于默认的冒泡阶段监听;
  • 基于 window 全局监听:它注册在 window 上而不是某个 DOM 元素上,与浏览器对 beforeunload 必须在顶层 window 触发的要求一致。

需要特别指出的是,callback 的返回值类型被声明为 any。历史上浏览器支持通过"返回字符串"来展示自定义提示文案,现代浏览器(Chrome、Firefox、Safari 等)出于反钓鱼考量已经一律忽略自定义文本,统一展示浏览器自带的通用提示。因此切勿把 callback 的返回值当作可展示给用户的文案。

3.1 它在包导出结构中的位置

从源码归属看,useBeforeUnload 是 DOM 层专属 Hook,位于 lib/dom 目录,而非核心路由引擎层。它经由 packages/react-router/index.tsexport { ... } from "./lib/dom/lib" 对外公开,因此使用方只需:

import { useBeforeUnload } from "react-router";

3.2 三种使用模式全部支持

根据 docs/start/modes.md 中记录的 API 可用性矩阵,useBeforeUnload 在框架模式、数据模式与声明式模式三列中均为 ✅,也就是说无论你用 createBrowserRouter + RouterProvider、传统的 <BrowserRouter> + <Routes>,还是基于 Vite 插件的框架路由,都可以直接使用该 Hook,无需任何适配。

四、配套的兄弟实现:pagehide / pageshow

值得顺带一提的是,在 useBeforeUnload 实现的紧邻位置,React Router 内部还定义了 usePageHideusePageShow 两个未导出的私有 Hook(见 packages/react-router/lib/dom/lib.tsx)。二者结构与 useBeforeUnload 完全同构,但监听的是 pagehidepageshow 事件。

源码注释中记录了一个重要的工程经验:pagehide 事件在"页面刷新前保存数据到 localStorage"这类场景下,跨浏览器支持程度优于 beforeunload;而 pageshow 事件携带的 persisted 标志,则用来判断文档是否是从浏览器往返缓存(bfcache)中恢复的。如果你需要处理的是"刷新前持久化"而非"卸载前拦截",可以沿这条思路评估哪种事件更可靠。usePageHide 的注释同样提醒:callback 参数应使用 React.useCallback() 创建。

五、最佳实践与常见误用

5.1 始终用 React.useCallback 稳定回调

由于 effect 依赖数组中包含 callback,最稳妥的用法是将其用 useCallback 缓存,并在依赖里加入影响行为的变量:

const isDirty = useFormDirtyFlag();
useBeforeUnload(
  useCallback((e: BeforeUnloadEvent) => {
    if (isDirty) {
      e.preventDefault();
    }
  }, [isDirty]),
);

5.2 认清边界:它拦不住 SPA 站内路由跳转

beforeunload 只在文档真正卸载时触发。React Router 的站内 <Link> 跳转、useNavigate 导航都属于 SPA 视图切换,不会卸载当前文档,因此不会触发该事件。若想在用户于站内导航时(比如从一个有未保存表单的路由跳走)进行拦截并给出自定义确认 UI,应使用 useBlocker 或由它包装而来的 unstable_usePrompt。两者职责的正确划分是:

场景 推荐方案
刷新 / 关闭标签页 / 跳转外部站点 useBeforeUnload(只能使用浏览器原生确认框)
SPA 站内路由切换,希望有自定义弹层 useBlocker(见其文档),或封装了 window.confirmunstable_usePrompt

需要注意的是,unstable_usePrompt 的源码注释中明确警告(见 packages/react-router/lib/dom/lib.tsx):当确认框弹出期间用户又点击了前进/后退按钮时,该方案在不同浏览器上表现差异巨大甚至可能出错,因此它的 unstable_ 前缀不会被移除,仅供自担风险地使用。

5.3 不要在 callback 中执行异步操作

beforeunload 事件是同步的,浏览器不会等待回调里的 Promise 完成。如果需要"尽力而为"地在卸载前发出异步请求,可考虑 navigator.sendBeacon(),但它超出了本 Hook 的能力范围——useBeforeUnload 只负责把同步回调安全地绑定到事件上。

六、小结

useBeforeUnload 是 React Router 对 Window 原生 beforeunload 事件的最小化、声明式封装:一条 useEffect、一对 addEventListener / removeEventListener、一个由 [callback, capture] 组成的依赖数组,就完成了从"手动管理全局监听"到"随组件生命周期自动清理"的转变。它同时覆盖框架、数据、声明式三种模式,全量支持见 docs/start/modes.md,最新 API 语义以 docs/api/hooks/useBeforeUnload.md 为准。使用时的核心心法是:callbackuseCallback 保持稳定;只做同步清理;并把"站内导航拦截"的工作交给 useBlocker 系列,让每个 Hook 都只在自己擅长的边界内发挥价值。

登录后查看全文
热门项目推荐
相关项目推荐