2026/9/15 19:58:21

TanStack Router 404 与错误处理完全指南:notFound、notFoundComponent、errorComponent 与路由掩码实战

TanStack Router 404 与错误处理完全指南:notFound、notFoundComponent、errorComponent 与路由掩码实战 TanStack Router 404 与错误处理完全指南notFound、notFoundComponent、errorComponent 与路由掩码实战【免费下载链接】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 中未找到Not Found与错误边界Error Boundary两大体系一类是 URL 路径无法匹配路由自动处理另一类是资源缺失如帖子 ID 不存在需开发者用notFound()手动触发同时覆盖按路由配置的errorComponent错误边界以及用于弹窗/临时页面场景的路由掩码Route Masking机制。读完本文你将掌握全局/路由级/路由级 404 组件的配置方式、notFoundMode的 fuzzy/root 差异、错误重试的正确姿势router.invalidate()而非reset()以及命令式与声明式两种掩码实现方案。两类未找到与一条统一 APITanStack Router 将 not found 划分为两种截然不同的来源但底层共用同一套notFound函数与notFoundComponentAPI见官方指南 not-found-errors.md路径未匹配自动访问的 pathname 不匹配任何已知路由模式或部分匹配但带有多余路径段。例如访问/users而路由树中没有该路由或访问/posts/1/edit而路由树只处理/posts/$postId。这类错误由路由器自动抛出。资源缺失手动例如访问/posts/1时 ID 为 1 的帖子不存在。开发者需要在beforeLoad或loader中使用notFound工具手动抛出。两者的共同点是都会向上寻找最近的、具备notFoundComponent的路由边界来渲染 404 UI。notFound()的源码本质在 not-found.ts 中notFound的实现非常轻量export function notFound(options: NotFoundError {}) { ;(options as any).isNotFound true if (options.throw) throw options return options } export function isNotFound(obj: any): obj is NotFoundError { return obj?.isNotFound true }它本质上只是给选项对象打上isNotFound标记并返回或立即抛出。NotFoundError支持以下选项选项类型说明routeIdRouteIds指定由哪个路由的边界处理如notFound({ routeId: /_layout })rootRouteId常量等价于routeId: rootRouteId强制根路由处理dataany向notFoundComponent转发部分数据throwboolean为true时直接抛出而非返回headersHeadersInit可携带响应头global/_globalboolean已弃用/内部使用请改用routeId: rootRouteIdisNotFound则被框架内部用于区分未找到错误与普通异常——这正是CatchNotFound能够只拦截 not-found、放行其他错误的关键。三层 404 组件配置1. 全局 404根路由的notFoundComponent在__root.tsx上挂载notFoundComponent是最常见的全局兜底方案它作为整个应用的最终边界// src/routes/__root.tsx import { createRootRoute, Outlet, Link } from tanstack/react-router export const Route createRootRoute({ component: () Outlet /, notFoundComponent: () { return ( div h1404 — Page Not Found/h1 Link to/Go Home/Link /div ) }, })2. 路由级默认defaultNotFoundComponent若希望所有有子路由的页面都自动获得默认 404 UI可将defaultNotFoundComponent传入createRouter// src/router.tsx import { createRouter } from tanstack/react-router import { routeTree } from ./routeTree.gen const router createRouter({ routeTree, defaultNotFoundComponent: () { return ( div pNot found!/p Link to/Go home/Link /div ) }, })注意defaultNotFoundComponent只对有子路由的节点生效。官方文档明确解释叶子节点路由没有子路由永远不渲染Outlet因此没有能力承接 not-found 错误。该选项在 packages/react-router/src/router.ts 的类型声明中标注默认值为NotFound即框架内置的极简pNot Found/p实现见 not-found.tsx 的DefaultGlobalNotFound。官方文档也直言这个默认组件刻意保持简陋强烈建议至少给根路由挂一个notFoundComponent或配置defaultNotFoundComponent。3. 路由级 404缺失资源场景当某个资源不存在时在loader或beforeLoad中抛出notFound()由该路由或其最近祖先的notFoundComponent处理// src/routes/posts.$postId.tsx import { createFileRoute, notFound } from tanstack/react-router import { getPost } from ../api export const Route createFileRoute(/posts/$postId)({ loader: async ({ params: { postId } }) { const post await getPost(postId) if (!post) throw notFound() return { post } }, component: PostComponent, notFoundComponent: ({ data }) { const { postId } Route.useParams() return pPost {postId} not found/p }, }) function PostComponent() { const { post } Route.useLoaderData() return h1{post.title}/h1 }notFound()的用法与redirect()类似——直接throw即可中断 loader 执行其后的代码可安全假定资源存在。也可用notFound({ throw: true })让函数自身抛出。指定处理边界routeId与rootRouteId默认情况下notFound()由最近的可处理祖先接管。若希望强制某个特定父路由处理传入routeId// src/routes/_layout/posts.$postId.tsx export const Route createFileRoute(/_layout/posts.$postId)({ loader: async ({ params: { postId } }) { const post await getPost(postId) if (!post) throw notFound({ routeId: /_layout }) return { post } }, })若希望直接打到根路由使用导出的rootRouteId常量类型上会自动补全合法的路由 IDimport { createFileRoute, notFound, rootRouteId } from tanstack/react-router export const Route createFileRoute(/posts/$postId)({ loader: async ({ params: { postId } }) { const post await getPost(postId) if (!post) throw notFound({ routeId: rootRouteId }) return { post } }, })notFoundModefuzzy 与 root 的边界归属当路由匹配失败时notFoundMode决定由谁渲染 404。该选项在 router.ts 中声明默认值为fuzzy。fuzzy默认尽量保留父级布局路由器会寻找最近的、有子路由且具备notFoundComponent或存在defaultNotFoundComponent的匹配祖先。例如路由树为__root__→posts→$postId三者都配了notFoundComponent访问/posts/1/edit时渲染Root Posts Post Post.notFoundComponent这样能最大限度保留用户预期到达位置附近的布局提供更多导航上下文。root统一交给根路由const router createRouter({ routeTree, notFoundMode: root, })此时所有路径型 not-found 都直接渲染Root.notFoundComponent跳过中间的 fuzzy 匹配。源码佐证findGlobalNotFoundRouteId在 router.ts 中findGlobalNotFoundRouteId精确实现了上述逻辑function findGlobalNotFoundRouteId( notFoundMode: root | fuzzy | undefined, routes: ReadonlyArrayAnyRoute, ) { if (notFoundMode ! root) { let fallback for (let i routes.length - 1; i 0; i--) { const route routes[i]! if (route.options.notFoundComponent) { return route.id } fallback || route.children route.id } if (fallback) return fallback } return rootRouteId }可以看到fuzzy 模式从匹配列表从深到浅扫描优先返回第一个定义了notFoundComponent的路由若都没有则回退到最深的有子路由的节点保持向后兼容root模式则直接返回rootRouteId。这套边界归属行为被测试 issue-6351-fuzzy-notfound-layout.test.ts 完整覆盖——该测试断言 fuzzy 404 必须归属到最深的有子路由且定义了notFoundComponent的匹配祖先否则会错误地跳过 pathless layout 的notFoundComponent并退化为defaultNotFoundComponent。错误边界errorComponent与 not-found 并行的是普通运行时错误。每个路由可配置errorComponent它接收error、info组件栈与reset三个属性// src/routes/posts.$postId.tsx import { createFileRoute, useRouter } from tanstack/react-router export const Route createFileRoute(/posts/$postId)({ loader: async ({ params: { postId } }) { const res await fetch(/api/posts/${postId}) if (!res.ok) throw new Error(Failed to load post) return res.json() }, component: PostComponent, errorComponent: PostErrorComponent, }) function PostErrorComponent({ error }: { error: unknown }) { const router useRouter() return ( div pError: {error instanceof Error ? error.message : String(error)}/p button onClick{() { // Invalidate 会重新执行 loader 并自动重置错误边界 router.invalidate() }} Retry /button /div ) } function PostComponent() { const data Route.useLoaderData() return h1{data.title}/h1 }全局默认错误组件与defaultNotFoundComponent对称createRouter也支持defaultErrorComponent默认值为内置ErrorComponent类型声明见 react-router/src/router.tsconst router createRouter({ routeTree, defaultErrorComponent: ({ error }) { const router useRouter() return ( div p Something went wrong:{ } {error instanceof Error ? error.message : String(error)} /p button onClick{() router.invalidate()}Retry/button /div ) }, })为什么重试要用router.invalidate()而不是reset()errorComponent的reset属性只负责清除错误边界内部状态见 CatchBoundary.tsx 中reset () this.setState({ error: 0 })它不会重新运行 loader。而router.invalidate()会触发全部或指定路由的 loader 重新执行并在数据就绪后自动重置边界。因此针对loader 抛错的重试场景正确姿势始终是invalidate()若只调用reset()页面会立即再次抛错回到错误态。CatchBoundary的类组件实现packages/react-router/src/CatchBoundary.tsx 中错误边界是一个 React 类组件getDerivedStateFromProps依据getResetKey()的变化自动清除错误态路由切换时自动恢复getDerivedStateFromError捕获任意抛出的值并包装成[error]数组——包装保证了任何可抛出的值包括 falsy都保持 truthy从而被状态正确识别componentDidCatch触发onCatch回调渲染时若处于错误态则创建errorComponent缺省为内置ErrorComponent并注入error与reset属性。在组件中抛出 not-foundCatchNotFound官方推荐在 loader 中抛 not-found便于类型推导、避免闪烁但组件内同样可以抛。此时使用CatchNotFound组件包裹import { CatchNotFound } from tanstack/react-router function Photo() { return ( CatchNotFound fallback{(error) pPhoto not found/p} onCatch{(error, errorInfo) { // 上报到监控平台 }} PhotoContent / /CatchNotFound ) }not-found.tsx 的实现展示了它的关键行为内部复用一个CatchBoundary其errorComponent用isNotFound(error)判断——只有 not-found 错误才走fallback其他错误原样重新抛出交给上层边界。服务端渲染时reset key 由pathname status计算保证 SSR 与客户端行为一致。notFoundComponent中的数据限制notFoundComponent中不能可靠使用useLoaderData——loader 可能尚未完成或根本没有执行。安全的 hooks 是useParams、useSearch、useRouteContextnotFoundComponent: ({ data }) { // SAFE — always available: const params Route.useParams() const search Route.useSearch() const context Route.useRouteContext() // UNSAFE — may be undefined: // const loaderData Route.useLoaderData() return pItem {params.id} not found/p }通过data选项转发部分数据若需要在 404 UI 中展示部分加载结果可通过notFound()的data选项转发并在组件内做类型收窄export const Route createFileRoute(/posts/$postId)({ loader: async ({ params }) { const partialData await getPartialData(params.id) if (!partialData.fullResource) { throw notFound({ data: { name: partialData.name } }) } return partialData }, notFoundComponent: ({ data }) { // data 的类型是 unknown需要自行校验 const info data as { name: string } | undefined return p{info?.name ?? Resource} not found/p }, })路由掩码Route MaskingURL 与真实路由分离路由掩码让地址栏显示的 URL与实际渲染的路由不一致典型场景是弹窗导航到/photo/5/modal但地址栏显示/photos/5或导航到带?showLogintrue的页面但掩掉 search 参数。原理location.state.__tempLocation掩码数据存储在历史条目的location.state中。当路由器从 history 解析 location 时若发现__tempLocation属性就会用其中的运行时 location 替代 URL 解析出的 location同时把历史 location 保存到maskedLocation供 Devtools 等识别当前 URL 被掩码了。关键推论掩码数据一旦脱离本地历史栈复制、分享、新标签页打开就会丢失浏览器会直接访问可见的被掩码的URL——这恰恰是掩码的目的。命令式掩码Link与useNavigatemask选项接受与Link/navigate()相同的导航对象to、params、search、replace、state等且完全类型安全import { Link } from tanstack/react-router function PhotoGrid({ photoId }: { photoId: string }) { return ( Link to/photos/$photoId/modal params{{ photoId }} mask{{ to: /photos/$photoId, params: { photoId }, }} Open Photo /Link ) }import { useNavigate } from tanstack/react-router function OpenPhotoButton({ photoId }: { photoId: string }) { const navigate useNavigate() return ( button onClick{() navigate({ to: /photos/$photoId/modal, params: { photoId }, mask: { to: /photos/$photoId, params: { photoId }, }, }) } Open Photo /button ) }声明式掩码createRouteMask若同一掩码规则在多处复用可以注册到路由器的routeMasks选项避免在每一个Link上重复书写import { createRouter, createRouteMask } from tanstack/react-router import { routeTree } from ./routeTree.gen const photoModalMask createRouteMask({ routeTree, from: /photos/$photoId/modal, to: /photos/$photoId, params: (prev) ({ photoId: prev.photoId }), }) const router createRouter({ routeTree, routeMasks: [photoModalMask], })createRouteMask必须提供routeTree、from被掩码的路由 ID以及标准导航选项to、search、params、replace等params可以是函数接收上一个参数并返回新参数同样类型安全。unmaskOnReload刷新时是否解开掩码默认情况下本地刷新页面时 URL保持掩码状态因为历史栈仍在内存中。若希望刷新后回到真实 URL有三个层级优先级从低到高// 1. 路由器级默认 const router createRouter({ routeTree, unmaskOnReload: true, }) // 2. 单条 route mask const mask createRouteMask({ routeTree, from: /photos/$photoId/modal, to: /photos/$photoId, params: (prev) ({ photoId: prev.photoId }), unmaskOnReload: true, }) // 3. 单次导航优先级最高 Link to/photos/$photoId/modal params{{ photoId }} mask{{ to: /photos/$photoId, params: { photoId } }} unmaskOnReload Open Photo /Link在 router.ts 中可以看到nextHistory.unmaskOnReload ?? this.options.unmaskOnReload ?? false正是按导航级 → 路由器级 → 默认 false的优先级解析BuildNextOptions.mask也声明了unmaskOnReload字段见 router.ts。常见错误清单务必规避1. HIGH还在用已弃用的NotFoundRoute// WRONG — NotFoundRoute 会阻止 notFound() 和 notFoundComponent 生效 import { NotFoundRoute } from tanstack/react-router const notFoundRoute new NotFoundRoute({ component: () p404/p }) const router createRouter({ routeTree, notFoundRoute }) // CORRECT — 在根路由上使用 notFoundComponent export const Route createRootRoute({ component: () Outlet /, notFoundComponent: () p404/p, })NotFoundRoute是路由而非组件它依赖父路由的Outlet才能渲染无法使用布局且路径匹配是宽松的/post/1/2/3会匹配NotFoundRoute。而notFoundComponent可挂载到任意路由、兼容布局且路径匹配严格/post/1/2/3对$postId是多余的路径段会触发 not-found。迁移只需两步从createRouter移除notFoundRoute给根路由或其他路由加上notFoundComponent。源码中 router.ts 会对仍使用notFoundRoute的配置打印弃用警告。注意notFoundComponent不支持渲染Outlet。2. MEDIUM在notFoundComponent中读取useLoaderDataloader 可能没执行完数据为undefined。改用useParams/useSearch/useRouteContext或通过notFound({ data })显式转发。3. MEDIUM指望叶子路由捕获路径型 404只有有子路由渲染Outlet的路由才能承接路径不匹配的 404。/posts/$postId这样的叶子路由上的notFoundComponent只对本路由 loader 中抛出的notFound()生效未匹配的子路径会冒泡到最近的父级边界。4. MEDIUM以为掩码 URL 能跨分享存活掩码数据在location.state中复制/分享/新标签页打开后即丢失浏览器会直接导航到可见 URL。5. HIGH跨技能只用reset()重试 loader 错误reset()只清错误态不重跑 loader应使用router.invalidate()触发重新加载并自动重置边界。相关源码与测试指引notFound/isNotFound实现packages/router-core/src/not-found.tsnotFoundMode与findGlobalNotFoundRouteIdpackages/router-core/src/router.tsCatchBoundary/ErrorComponentpackages/react-router/src/CatchBoundary.tsxCatchNotFound/DefaultGlobalNotFoundpackages/react-router/src/not-found.tsxdefaultErrorComponent/defaultNotFoundComponent类型声明packages/react-router/src/router.tsfuzzy 404 边界归属测试packages/router-core/tests/issue-6351-fuzzy-notfound-layout.test.tsloader 中notFound()根边界测试packages/router-core/tests/issue-4078-loader-notfound-root-boundary.test.tsSSR 场景 not-found 测试packages/router-core/tests/server-serial-ssr-notfound.test.ts官方指南docs/router/guide/not-found-errors.md 与 docs/router/guide/route-masking.md交叉参考router-core/data-loadingloader 中抛出notFound()与错误边界、loader 数据可用性相互影响errorComponent重试依赖router.invalidate()。router-core/type-safetynotFoundComponent收到的data类型为unknown使用前必须校验收窄。【免费下载链接】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),仅供参考