React Router useBeforeUnload Hook 详解:在页面卸载前拦截并执行清理逻辑
React Router 提供的 useBeforeUnload Hook 让组件能够在浏览器窗口真正被关闭、刷新或跳离页面之前,声明式地注册一个回调函数,用于监听 Window 的 beforeunload 事件。它适用于框架模式(framework)、数据路由模式(data)与声明式路由模式(declarative)三种使用场景。读完本文,你将掌握 useBeforeUnload 的完整签名、参数语义与底层实现原理,并能正确区分它与 useBlocker / unstable_usePrompt 的职责边界,写出可靠的"离开前兜底"逻辑。
一、这个 Hook 解决什么问题
在 React 单页应用中,"离开页面"其实有两种截然不同的含义:
- 站内路由跳转:例如从
/dashboard跳到/settings,此时页面并没有被卸载,SPA 只是替换了视图; - 真正的页面卸载:用户点击刷新、关闭标签页,或跳转到外部网站,此时整个文档会被销毁。
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.ts 的 export { ... } 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 内部还定义了 usePageHide 与 usePageShow 两个未导出的私有 Hook(见 packages/react-router/lib/dom/lib.tsx)。二者结构与 useBeforeUnload 完全同构,但监听的是 pagehide 与 pageshow 事件。
源码注释中记录了一个重要的工程经验: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.confirm 的 unstable_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 为准。使用时的核心心法是:callback 用 useCallback 保持稳定;只做同步清理;并把"站内导航拦截"的工作交给 useBlocker 系列,让每个 Hook 都只在自己擅长的边界内发挥价值。
atomcodeClaude Code 的开源替代方案。连接任意大模型,编辑代码,运行命令,自动验证 — 全自动执行。用 Rust 构建,极致性能。 | An open-source alternative to Claude Code. Connect any LLM, edit code, run commands, and verify changes — autonomously. Built in Rust for speed. Get StartedRust0627
Hy4-previewHy4 preview 是由腾讯混元团队研发的新一代混合专家(MoE)旗舰模型。模型总参数量 770B,每个 token 激活 49B,主干共包含78层,第一层采用标准 FFN,其余 77 层均为 MoE 结构,每层包含 256 个路由专家与 1 个共享专家,每个 token 激活 top-8 路由专家及共享专家。主干之外原生内置 1 层 MTP(总参数量 10B,激活 0.7B)以支持投机解码。Python00
GLM-5.3GLM-5.3 与 GLM-5.2 使用相同的基座模型——所有提升均来自后训练。与 GLM-5.2 相比,它在复杂编程和长程任务上的表现显著提升。Jinja00
GLM-5.3-FlashGLM-5.3-Flash (320B-A18B),是GLM-5系列的首个原生多模态模型。320B总参数,能力超过GLM-5.2Jinja00
Spark-X2.5-4BSpark-X2.5-4B 旨在让强大的 AI 更实用、更高效、更易获得。在广泛日常任务中表现强劲,涵盖对话、写作、翻译、推理、编码、工具调用以及智能体工作流,并在同等规模的开源模型中取得领先成绩。Spark-X2.5 将面向效率的架构与最高 1M tokens 的原生上下文窗口相结合,并支持 200 多种语言。Python00
Spark-X2.5-1.7BSpark-X2.5-1.7B 旨在让强大的 AI 更加实用、高效且易于获取。这些模型在广泛的日常任务中表现出色,涵盖对话、写作、翻译、推理、编程、工具调用和智能体工作流,并在同等规模的开源模型中取得领先结果。Spark-X2.5 将面向效率的架构与最高 1M tokens 的原生上下文窗口相结合,并支持 200 多种语言。Python00