TanStack Router 路由事件订阅指南:用 router.subscribe 精准感知导航生命周期
【免费下载链接】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
导读
TanStack Router 通过router.subscribe对外暴露完整的路由生命周期事件,让开发者可以以命令式方式监听"导航开始、路由加载、组件挂载、导航完成"等关键节点。这篇指南将带你掌握router.subscribe的完整用法:事件列表、触发顺序、事件载荷(payload)字段语义,以及埋点统计、外部状态清理、DOM 操作等真实场景的落地写法;同时结合本仓库 router-core 的源码实现,说明这些事件在内部到底是如何被计算和发射的。
基本用法:订阅与取消订阅
router.subscribe接收两个参数:事件名与监听器函数,并返回一个unsubscribe函数用于移除监听:
const unsubscribe = router.subscribe('onResolved', (event) => { console.info('Navigation finished:', event.toLocation.href) }) // 稍后清理监听器 unsubscribe()从源码看,subscribe的实现非常轻量(packages/router-core/src/router.ts#L1407-L1418):它将{ eventType, fn }包装成一个 listener 放入内部的subscribers集合(Set),返回的闭包负责从集合中删除该 listener。这意味着:
- 同一事件可以注册多个监听器,它们都会被依次调用;
- 返回的
unsubscribe是幂等且安全的——即使重复调用也不会报错; - 监听器内部抛出的异常会被
emit捕获并console.error(router.ts#L1420-L1430),不会中断其他监听器,也不会影响路由本身的导航流程。
何时该用 router.subscribe
router.subscribe最适合那些"需要观察导航、但不需要驱动渲染"的命令式集成场景:
- 埋点与页面访问(pageview)统计
- 重置外部缓存或 mutation 状态
- 记录导航耗时与过渡过程
- 在路由渲染完成后执行依赖 DOM 的逻辑(如聚焦标题、滚动定位)
如果你需要的是响应式的 UI 更新,则应当优先使用框架 Hooks(如useRouterState、useSearch、useParams),而不是手动订阅。订阅是命令式的一次性回调,不会触发组件重渲染;Hooks 则会与渲染周期绑定,自动在状态变化时重渲染。
可用事件一览
TanStack Router 会发射以下生命周期事件:
| 事件名 | 触发时机 |
|---|---|
onBeforeNavigate | 一次导航即将开始之前 |
onBeforeLoad | 路由加载开始之前 |
onLoad | 下一个 location 已提交、路由匹配(matches)已加载完成之后 |
onBeforeRouteMount | 加载完成后、路由组件即将挂载之前 |
onResolved | 导航完全解析(resolve)之后 |
onRendered | 路由渲染完成之后 |
此外,事件类型中还存在一个onInjectedHtml事件(仅在服务端 SSR 场景由ssr-server触发,用于注入 HTML 流时回调),完整的事件载荷类型定义见 RouterEventsType 类型文档。每个事件的 payload 都以事件名作为type字段,可用于在同一个监听函数内进行可辨识联合(discriminated union)判断:
router.subscribe('onResolved', (event) => { if (event.type === 'onResolved') { // 类型被收窄为 onResolved 的载荷 } })典型事件流
对于一次正常导航,事件通常按以下顺序流动:
onBeforeNavigateonBeforeLoadonLoadonBeforeRouteMountonResolvedonRendered
在客户端实现中(packages/router-core/src/load-client.ts#L1952-L1958),onBeforeNavigate与onBeforeLoad在路由匹配与资源获取之前同步发射;而在匹配加载完成、location 提交之后(load-client.ts#L1877-L1909),依次发射onLoad、onBeforeRouteMount,并在resolvedLocation更新、router 状态回到idle后发射onResolved与onRendered。
服务端(SSR)路径同样会发射onBeforeNavigate与onBeforeLoad(见 packages/router-core/src/load-server.ts#L891-L894),因此基于这两个事件编写的"导航开始"类逻辑在服务端渲染时也能生效。
需要注意:
onBeforeNavigate与onBeforeLoad之间存在一个 preflight 中断检查(load-client.ts#L1955-L1958)——如果导航被更快的后续导航取代(abort),onBeforeLoad可能不会发射。因此,若必须在"加载开始"时做副作用,建议以onBeforeNavigate为准,或同时兼容两个事件。
你通常不需要监听所有事件。经验法则:
- 用
onBeforeNavigate或onBeforeLoad观察导航开始 - 用
onResolved做埋点统计与导航完成后的清理 - 用
onRendered做依赖 DOM 的工作
事件载荷:location 变化元数据
导航类事件都会携带描述"从哪到哪、发生了什么变化"的元数据:
const unsubscribe = router.subscribe('onBeforeNavigate', (event) => { console.info({ from: event.fromLocation?.href, to: event.toLocation.href, pathChanged: event.pathChanged, hrefChanged: event.hrefChanged, hashChanged: event.hashChanged, }) })几个关键细节:
fromLocation在首次加载时可能是undefined(因为不存在上一个已解析的 location);pathChanged表示pathname 是否变化;hrefChanged覆盖pathname、search、hash 的任意变化;hashChanged用于区分仅 hash 变化的导航(例如锚点跳转)。
这些布尔标志的语义直接来自源码中的getLocationChangeInfo(packages/router-core/src/router.ts#L926-L937):
export function getLocationChangeInfo(location, resolvedLocation) { return { fromLocation: resolvedLocation, toLocation: location, pathChanged: resolvedLocation?.pathname !== location.pathname, hrefChanged: resolvedLocation?.href !== location.href, hashChanged: resolvedLocation?.hash !== location.hash, } }即:pathChanged是比较新旧pathname,hrefChanged是比较完整href,hashChanged是比较hash。fromLocation/toLocation的类型为ParsedLocation(解析后的强类型 location,包含pathname、search、hash、href、state等字段,详见 ParsedLocation 类型文档),因此你可以在监听器里安全地读取这些结构化字段,而不必手动解析 URL 字符串。
常见模式
跟踪页面访问(pageview)
onResolved是埋点的首选事件,因为它在导航完全结束后才触发,此时toLocation即最终落地的页面:
const unsubscribe = router.subscribe('onResolved', ({ toLocation }) => { analytics.track('page_view', { path: toLocation.pathname, href: toLocation.href, }) })清除外部 mutation 状态
如果你使用的 mutation 库没有按 key 隔离的 mutation 状态(例如一个全局的 mutation cache),可以在导航后清理——但只应在路径确实变化时清理,避免同路径下的 search/hash 变化误伤:
const unsubscribe = router.subscribe('onResolved', ({ pathChanged }) => { if (pathChanged) { mutationCache.clear() } })执行依赖 DOM 的逻辑
当副作用依赖"新路由内容已经在 DOM 中"时,使用onRendered:
const unsubscribe = router.subscribe('onRendered', ({ toLocation }) => { focusPageHeading(toLocation.pathname) })onRendered的发射条件带有rendered判断(load-client.ts#L1908-L1910),确保只有真正完成渲染的提交才会触发,因此比onResolved更适合 DOM 操作。事实上,TanStack Router 内置的滚动恢复功能正是依赖onBeforeLoad记录滚动位置、onRendered恢复滚动位置(见 packages/router-core/src/scroll-restoration.ts#L216 与 scroll-restoration.ts#L239),可以作为"官方同款"的订阅范例参考。
在组件中正确取消订阅
如果你在组件或框架的 effect 中订阅,一定要把返回的unsubscribe函数作为清理函数返回,确保组件卸载时监听器被移除,避免内存泄漏与重复触发:
import { useEffect } from 'react' import { useRouter } from '@tanstack/react-router' function PageViewTracker() { const router = useRouter() useEffect(() => { return router.subscribe('onResolved', ({ toLocation }) => { analytics.track('page_view', { path: toLocation.pathname }) }) }, [router]) return null }由于subscribe返回的正是清理函数本身,直接return router.subscribe(...)即可满足 React 对 effect 清理函数的要求。
与路由 Hooks 的分工
router.subscribe解决的是"过程观察"问题,而useRouterState、useSearch、useParams等 Hooks 解决的是"状态渲染"问题:
- 需要渲染当前 location、search、params → 用 Hooks(useRouterStateHook、useSearchHook、useParamsHook);
- 需要在导航过程中执行一次性副作用→ 用
router.subscribe; - 需要读取当前 router 状态快照(如
resolvedLocation、status)→ 参考 RouterState 类型文档 与 Router 类型文档。
小结
router.subscribe是 TanStack Router 面向命令式集成场景的官方事件通道,覆盖了从导航开始到渲染完成的完整生命周期。通过本文介绍的六个事件、标准事件流、payload 字段语义与三类常见模式,你可以把埋点统计、缓存清理、DOM 操作等副作用干净地挂接到路由上;结合 router-core 源码 中subscribe/emit/getLocationChangeInfo的实现,还能准确预判不同导航(首次加载、路径变化、hash-only 变化、被取代的导航)下的事件行为。
如果你的侧重点是在导航过程中同步修改数据(例如加载完成后的数据变更流程),可以进一步阅读 数据变更指南,它与本文的路由事件机制共同构成 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),仅供参考