TanStack Router 如何用 useBlocker 阻止用户离开未保存表单的页面
【免费下载链接】router🤖 A client-first, server-capable, fully type-safe router and full-stack framework for the web (React and more).项目地址: https://gitcode.com/GitHub_Trending/ro/router
在 React 应用里有一个很常见的坑:用户在一个带表单的页面编辑到一半,随手点了导航链接或浏览器后退,表单内容直接丢失。TanStack Router 的useBlockerhook(来自@tanstack/react-router)就是为这类场景设计的:当导航发生时先执行你的拦截逻辑,让用户确认「是否离开」,确认后导航照常进行,取消则所有待处理的导航被阻断。
注意useBlocker目前的 API 在官方文档中标注为experimental(见 useBlocker API 文档),升级版本时留意其变化。
useBlocker 拦截的是什么
根据 Navigation Blocking 指南,导航阻断分两层生效:
- 应用内导航:由路由控制的导航(点
Link、navigate调用、前进后退等)。每个 blocker 的shouldBlockFn会被异步、顺序地执行。任何一个 blocker 返回false即放行;只要有一个返回true,导航就被取消。 beforeunload事件:对路由管不到的页面卸载操作——关闭标签页/窗口、刷新——依赖浏览器的原生「Are you sure you want to leave?」对话框。用户确认则所有 blocker 被绕过,取消则页面保持原样。
所以useBlocker同时覆盖了「应用内跳转」和「刷新/关页」两条会丢数据的路径。
最短路径:用 window.confirm 拦截
如果只是需要一个确认弹窗,最快的写法是在shouldBlockFn里直接用window.confirm,不需要 resolver。shouldBlockFn返回true表示拦截本次导航,返回false表示放行(这是 API 文档 中明确的语义):
import { useBlocker } from '@tanstack/react-router' function MyComponent() { const [formIsDirty, setFormIsDirty] = useState(false) useBlocker({ shouldBlockFn: () => { if (!formIsDirty) return false const shouldLeave = confirm('Are you sure you want to leave?') return !shouldLeave }, }) // ... }这里的formIsDirty是你在表单变更时维护的状态,setFormIsDirty在输入/修改等地方调用即可。这个例子里不传withResolver(默认false),拦截结果完全由shouldBlockFn的返回值决定,hook 返回void。
自定义确认 UI:withResolver
大多数实际项目会用与自身设计一致的弹窗或内联确认区,这时用withResolver: true。开启后 hook 返回一个控制对象,shouldBlockFn只负责判断「该不该弹确认」,真正放行/取消由返回的proceed/reset完成(注意:withResolver为true时,shouldBlockFn的返回值不会解决阻断,见 指南):
import { useBlocker } from '@tanstack/react-router' function MyComponent() { const [formIsDirty, setFormIsDirty] = useState(false) const { proceed, reset, status, next } = useBlocker({ shouldBlockFn: () => formIsDirty, withResolver: true, }) return ( <> {/* 你的表单 */} {status === 'blocked' && ( <div> <p>You are navigating to {next.pathname}</p> <p>Are you sure you want to leave?</p> <button onClick={proceed}>Yes</button> <button onClick={reset}>No</button> </div> )} </> ) }返回值的含义(API 文档):
status:'blocked'或'idle','blocked'时next、current、action才有值,分别给出目标位置、当前位置和触发导航的 action;proceed:允许导航继续;reset:取消导航,status复位为'idle'。
shouldBlockFn的参数是类型安全的,包含current和next两个位置对象(各有routeId、fullPath、pathname、params、search)以及action。可以用它做更精细的判断,例如只在离开特定路由时拦截:
const { proceed, reset, status } = useBlocker({ shouldBlockFn: ({ current, next }) => { if ( current.routeId === '/editor-1' && next.fullPath === '/foo/$id' && next.params.id === '123' && next.search.hello === 'world' ) { return true } return false }, enableBeforeUnload: false, withResolver: true, })上面这段与官方 navigation-blocking 示例 中根路由的拦截逻辑一致。
控制浏览器的 beforeunload 弹窗
enableBeforeUnload选项默认是true,即始终拦截浏览器的beforeUnload事件。如果你只想在表单确实有改动时才弹原生确认框(避免表单干净时刷新页面也弹窗),可以传一个布尔值或函数:
useBlocker({ shouldBlockFn: () => formIsDirty, enableBeforeUnload: formIsDirty, // or () => formIsDirty })相关选项汇总(均见 useBlocker API 文档):
shouldBlockFn:必填,返回boolean或Promise<boolean>;disabled:可选,默认false,整体禁用该 blocker;enableBeforeUnload:可选,默认true,boolean | (() => boolean);withResolver:可选,默认false;blockerFn、condition:已废弃(deprecated),新代码不要使用。
跑一遍官方示例验证效果
仓库里有一个完整示例 examples/react/navigation-blocking,其中Editor1Component就是一个「输入框有内容即视为未保存」的未保存表单拦截场景(源码见 main.tsx):
// Block leaving editor-1 if there is text in the input const { proceed, reset, next, current, status } = useBlocker({ shouldBlockFn: () => value !== '', enableBeforeUnload: () => value !== '', withResolver: true, })按 示例 README 的步骤运行:
pnpm install pnpm devdev脚本实际执行的是vite --port 3000,所以开发服务器起在 3000 端口。在浏览器里可以逐项验证:
- 进入 Editor 1 页面,在输入框里输入文字;
- 点击「Go to Editor 2」或顶部指向其他路由的链接——页面停在原地,出现「Are you sure you want to leave editor 1?」确认区,并同时显示
You are going from {current.pathname} to {next.pathname}; - 点NO(
reset):导航取消,留在原页;点YES(proceed):导航继续; - 输入框为空时直接点链接,不出现确认区,导航直接通过;
- 有内容时刷新页面或关闭标签页,出现浏览器原生确认对话框——这正是
enableBeforeUnload: () => value !== ''在起作用。
根路由上还挂了第二个 blocker,只针对「从/editor-1导航到/foo/123?hello=world」这一条具体路径做拦截,示例里点击顶栏的 foo 123 链接即可复现,用来观察多个 blocker 并存的行为。
替代写法与限制
- 不想在逻辑里写 hook 的话,可以用
<Block>组件达到同样效果:shouldBlockFn作为 prop 传入,配合withResolver时以 render props 形式拿到{ status, proceed, reset }(见 指南的 Component-based blocking 一节)。 - 应用内拦截由
shouldBlockFn控制,关页/刷新这类浏览器原生事件只能借助beforeunload的通用对话框,无法在其中渲染自定义 UI。 shouldBlockFn也可以返回Promise<boolean>,官方指南里展示了在函数内打开自己管理的 modal、等用户点击后再 resolve 的异步写法,适合已有模态框管理的场景。
参考
- Navigation Blocking 指南
- useBlocker API 参考
- navigation-blocking 示例源码
- 手动搭建 React + TanStack Router 项目(如果你的项目还没引入 TanStack Router)
【免费下载链接】router🤖 A client-first, server-capable, fully type-safe router and full-stack framework for the web (React and more).项目地址: https://gitcode.com/GitHub_Trending/ro/router
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考