TanStack Router 404 与资源缺失处理全指南:notFound、notFoundComponent与notFoundMode深度解析
【免费下载链接】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 及配套的 React / Solid / Vue 框架实现)中 docs/router/guide/not-found-errors.md 展开,围绕路由匹配失败(404)与资源缺失两类场景,系统讲解
notFound函数、notFoundComponent路由选项、notFoundMode全局策略以及从旧版NotFoundRoute的迁移路径,并辅以 packages/router-core/src/not-found.ts、packages/router-core/src/router.ts 等源码证据,帮助读者在实战中正确配置、抛出并捕获 not-found 错误。
概述:not-found 错误的两大来源
在 TanStack Router 中,not-found 错误(404)有且仅有两种用途,二者在底层共用同一套notFound函数与notFoundComponentAPI:
路径不匹配(Non-matching route paths):当路径无法匹配任何已知路由模式,或者仅部分匹配某路由但带有额外路径段时,路由(Router)会自动抛出not-found 错误。
- 若路由的
notFoundMode为fuzzy(默认值),则由最近的可匹配路由中配置了notFoundComponent的那一个负责处理; - 若
notFoundMode为root,则统一由根路由负责处理。 - 典型示例:
- 访问
/users,但路由树中根本不存在/users路由; - 访问
/posts/1/edit,但路由树只声明了/posts/$postId(多出的/edit段无法匹配)。
- 访问
- 若路由的
资源缺失(Missing resources):当某个资源找不到,例如指定 ID 的文章不存在、异步数据不可用等场景。开发者需要自行在
beforeLoad或loader中调用notFound工具函数抛出 not-found 错误。- 当在
loader中调用时,由最近的、配置了notFoundComponent的路由处理;否则回退到根路由处理。 - 典型示例:
- 访问
/posts/1,但 ID 为 1 的文章不存在; - 访问
/docs/path/to/document,但该文档不存在。
- 访问
- 当在
从路由自动抛错到开发者手动抛错,最终都收敛为同一种错误对象与同一套渲染边界,这正是新 API 相比旧NotFoundRoute更统一、更可控的原因。
notFoundMode:全局 404 处理策略
当 TanStack Router 遇到无法匹配任何已知路由模式的 pathname,或部分匹配但存在多余尾部 pathname 段时,会自动抛出一个 not-found 错误。此时如何处理取决于路由器选项notFoundMode:
| 模式 | 行为 | 适用场景 |
|---|---|---|
fuzzy(默认) | 智能寻找最近的可匹配路由,渲染该路由的notFoundComponent | 尽量保留父级布局,让用户基于"原本想去的位置"就近获得导航上下文 |
root | 所有 not-found 错误一律由根路由的notFoundComponent处理 | 希望全局统一 404 页面、不关心就近路由 |
notFoundMode: 'fuzzy'(默认)
默认情况下notFoundMode为fuzzy:若 pathname 未匹配任何已知路由,路由器会尝试使用最近匹配的、且配置了notFoundComponent的路由。
为什么这是默认值?模糊匹配尽可能多地保留父级布局,用户能借助保留的上下文导航到有用位置,而不是直接跌落到一个与当前位置毫无关联的全新页面。
"最近合适路由"的查找标准为:该路由必须配置了notFoundComponent,或者路由器配置了defaultNotFoundComponent。
例如,给定如下路由树:
__root__(配置了notFoundComponent)posts(配置了notFoundComponent)$postId(配置了notFoundComponent)
若访问/posts/1/edit,将渲染如下组件结构:
<Root> <Posts> <Post> <Post.notFoundComponent>因为$postId是最近的、配置了notFoundComponent的匹配路由,所以渲染的是它的notFoundComponent。
这一查找逻辑在源码中有精确对应:findGlobalNotFoundRouteId(见 packages/router-core/src/router.ts)从已匹配路由数组的末尾(最深层路由)向前遍历,一旦发现route.options.notFoundComponent就立即返回该路由 id;若没有找到任何配置了notFoundComponent的路由,则回退到具有子路由的最近分支,最终仍无果时才回落到rootRouteId:
function findGlobalNotFoundRouteId( notFoundMode: 'root' | 'fuzzy' | undefined, routes: ReadonlyArray<AnyRoute>, ) { 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 }notFoundMode: 'root'
当notFoundMode设为root时,所有 not-found 错误都由根路由的notFoundComponent处理,而不是从最近模糊匹配的路由向上冒泡。以上述同一路由树为例,访问/posts/1/edit时将渲染:
<Root> <Root.notFoundComponent>因为模式为root,所以渲染的是__root__路由的notFoundComponent。从源码可见,root模式直接跳过最近的模糊匹配遍历,无条件返回rootRouteId。notFoundMode的默认值在 packages/router-core/src/router.ts 处确认:notFoundMode: options.notFoundMode ?? 'fuzzy'。
配置路由的notFoundComponent
为同时处理上述两类 not-found 错误,可以给任意路由挂载notFoundComponent。该组件会在 not-found 错误被抛出时渲染。
例如,为/settings路由配置notFoundComponent,处理不存在的设置子页面:
export const Route = createFileRoute('/settings')({ component: () => { return ( <div> <p>Settings page</p> <Outlet /> </div> ) }, notFoundComponent: () => { return <p>This setting page doesn't exist!</p> }, })或者为/posts/$postId路由配置notFoundComponent,处理不存在的文章:
export const Route = createFileRoute('/posts/$postId')({ loader: async ({ params: { postId } }) => { const post = await getPost(postId) if (!post) throw notFound() return { post } }, component: ({ post }) => { return ( <div> <h1>{post.title}</h1> <p>{post.body}</p> </div> ) }, notFoundComponent: () => { return <p>Post not found!</p> }, })注意这里的notFoundComponent不是路由组件,它在渲染时会被wrapInNonRouteComponentContext包裹(见 packages/react-router/src/renderRouteNotFound.tsx),因此不支持渲染<Outlet />,且与普通路由组件的数据访问方式存在差异(详见下文"notFoundComponent中的数据加载"一节)。
全局默认 not-found 处理:defaultNotFoundComponent
你可能会希望为应用中所有带有子路由的路由提供默认的 not-found 组件。
为什么只针对有子路由的路由?叶子路由(无子路由的路由)永远不会渲染
<Outlet>,因此无法处理 not-found 错误。
为此,把defaultNotFoundComponent传给createRouter即可:
const router = createRouter({ defaultNotFoundComponent: () => { return ( <div> <p>Not found!</p> <Link to="/">Go home</Link> </div> ) }, })该选项的声明位于 packages/react-router/src/router.ts。渲染优先级在 packages/react-router/src/renderRouteNotFound.tsx 中清晰可见:
- 优先使用路由自身的
notFoundComponent; - 未配置时回退到路由器级
defaultNotFoundComponent; - 两者都没有时,回退到 TanStack Router 内置的
DefaultGlobalNotFound——即那个刻意保持"极其简陋"(且故意不美观)的默认组件,仅渲染<p>Not Found</p>(实现见 packages/react-router/src/not-found.tsx)。
源码中还会在开发环境打印一条警告,提示开发者为路由配置notFoundComponent或路由器级defaultNotFoundComponent,以避免落到过度通用的默认组件。强烈建议至少为根路由挂载一个notFoundComponent,或配置路由器级defaultNotFoundComponent。
手动抛出notFound错误
除了路由自动抛错,你也可以在 loader 和组件中手动抛出 not-found 错误,用于标记"资源不存在"。notFound函数与redirect函数的工作方式类似——抛出notFound()即可触发 not-found 错误:
export const Route = createFileRoute('/posts/$postId')({ loader: async ({ params: { postId } }) => { // 文章不存在时返回 null const post = await getPost(postId) if (!post) { throw notFound() // 或者让 notFound 函数自行抛出: // notFound({ throw: true }) } // 走到这里,post 一定有值(因为不存在时已抛出错误) return { post } }, })从源码实现看(packages/router-core/src/not-found.ts),notFound函数的核心逻辑是给选项对象打上内部标记isNotFound = true,然后根据throw选项决定直接抛出还是返回错误对象;而isNotFound(obj)则用于运行时判别某个值是否是 TanStack Router 的 not-found 错误(obj?.isNotFound === true),这也是CatchNotFound等边界组件判断错误类型的依据:
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 }上述手动抛出的 not-found 错误,将由该路由自身或最近的、配置了notFoundComponent路由选项(或defaultNotFoundComponent路由器选项)的父级路由处理。
若既没有找到合适的路由,也没有合适的父级路由处理该错误,则根路由会使用 TanStack Router 内置的极简默认 not-found 组件(仅渲染<p>Not Found</p>)来处理。
在
beforeLoad中抛出时的行为:当你(或任何库代码)在beforeLoad中抛出notFound()时,TanStack Router 会像处理其他 not-found 错误一样解析它:
- 若传入了
routeId,由该路由(或最近的合法祖先边界)处理;- 若未传入
routeId,由最近的、配置了notFoundComponent的路由/祖先处理(取决于路由器的模式和匹配规则);- 若找不到合适的边界,则回退到根路由/默认 not-found 行为。
对于
beforeLoad中抛出的 not-found 错误,TanStack Router 仍会运行必需的父级 loader,以确保选中的 not-found 边界能带着它依赖的 loader 数据正常渲染。
指定哪些路由处理 not-found 错误
有时你希望在某个特定的父级路由上触发 not-found,并绕过常规的 not-found 组件传播逻辑。此时可在notFound函数的route选项中传入目标路由 id:
// _pathlessLayout.tsx export const Route = createFileRoute('/_pathlessLayout')({ // 这个会渲染 notFoundComponent: () => { return <p>Not found (in _pathlessLayout)</p> }, component: () => { return ( <div> <p>This is a pathless layout route!</p> <Outlet /> </div> ) }, }) // _pathlessLayout/route-a.tsx export const Route = createFileRoute('/_pathless/route-a')({ loader: async () => { // 让 LayoutRoute 处理这个 not-found 错误 throw notFound({ routeId: '/_pathlessLayout' }) // ^^^^^^^^^ 会从注册的 router 中自动补全 }, // 这个不会渲染 notFoundComponent: () => { return <p>Not found (in _pathlessLayout/route-a)</p> }, })routeId的类型为RouteIds<RegisteredRouter['routeTree']>(见 packages/router-core/src/not-found.ts),在配置了注册路由器的项目中,该字段会获得完整的类型自动补全与校验。
手动指定根路由
你也可以通过向notFound函数的route属性传入导出的rootRouteId变量来指定根路由:
export const Route = createFileRoute('/posts/$postId')({ loader: async ({ params: { postId } }) => { const post = await getPost(postId) if (!post) throw notFound({ routeId: rootRouteId }) return { post } }, })rootRouteId由 packages/router-core/src/root.ts 导出,并经由 packages/router-core/src/index.ts 对外暴露。值得留意的是,NotFoundError类型中有一个@deprecated的global选项(见 packages/router-core/src/not-found.ts),其注释明确建议改用routeId: rootRouteId——说明旧式的"全局 404"概念已被显式路由定位取代。
在组件中抛出 not-found 错误
你也可以在组件中抛出 not-found 错误。不过,官方建议优先在 loader 中抛出,以便正确推导 loader 数据类型并避免闪烁(flickering)。
TanStack Router 提供了与CatchBoundary类似的CatchNotFound组件,用于在组件中捕获 not-found 错误并展示对应 UI。其实现位于 packages/react-router/src/not-found.tsx:内部复用CatchBoundary,在onCatch与错误渲染分支中通过isNotFound(error)判断错误类型——若是 not-found 错误则执行fallback/onCatch,否则原样重新抛出;同时基于pathname与路由状态生成resetKey,确保导航到新路径后边界能够正确重置。
import { CatchNotFound } from '@tanstack/react-router' function Posts() { return ( <CatchNotFound fallback={(error) => <p>Post not found.</p>}> <PostDetail /> </CatchNotFound> ) }该组件已从 packages/react-router/src/index.tsx 对外导出(同时导出的还有DefaultGlobalNotFound)。
notFoundComponent中的数据加载
notFoundComponent在数据加载方面是一个特例:SomeRoute.useLoaderData可能未定义,具体取决于你正在访问哪个路由、以及 not-found 错误是在哪里抛出的。而Route.useParams、Route.useSearch、Route.useRouteContext等 Hook 会返回确定的值。
若需要把不完整的 loader 数据传给notFoundComponent,可通过notFound函数中的data选项传递,并在notFoundComponent中自行校验:
export const Route = createFileRoute('/posts/$postId')({ loader: async ({ params: { postId } }) => { const post = await getPost(postId) if (!post) throw notFound({ // 把部分数据转发给 notFoundComponent // data: someIncompleteLoaderData }) return { post } }, // 调用 notFound 时通过 data 选项传入的数据会以 { data } 形式到达组件 notFoundComponent: ({ data }) => { // ❌ 这里不能使用 useLoaderData:const { post } = Route.useLoaderData() // ✅ 这些 Hook 是安全的: const { postId } = Route.useParams() const search = Route.useSearch() const context = Route.useRouteContext() return <p>Post with id {postId} not found!</p> }, })对应的data字段定义在 packages/router-core/src/not-found.ts,类型为any;notFoundComponent渲染时该数据会作为 props 传入(见 packages/react-router/src/renderRouteNotFound.tsx)。注意NotFoundError中还支持headers?: HeadersInit字段,可用于在 SSR 等场景下附带响应头信息。
与 SSR 的配合
not-found 错误处理在服务端渲染(SSR)场景下同样生效:路由匹配失败或 loader 中抛出的notFound()会在服务端被捕获,并将状态同步到客户端完成一致的 404 渲染。详细的服务端渲染配置与流程请参阅 SSR 指南。
从源码结构看,服务端的加载流程同样参与了 not-found 判定:notFoundMode相关逻辑同时存在于 packages/router-core/src/load-client.ts、packages/router-core/src/load-server.ts 与 packages/router-core/src/router.ts 中,确保客户端与服务端对"由谁处理 404"的决策保持一致。
从NotFoundRoute迁移
NotFoundRouteAPI 已废弃,未来版本将移除,请改用notFoundComponent。
重要:使用
NotFoundRoute时,notFound函数与notFoundComponent将不会生效——二者是互斥的两套机制。
两者主要差异如下:
| 对比维度 | NotFoundRoute(已废弃) | notFoundComponent(推荐) |
|---|---|---|
| 形态 | 一个独立的路由 | 可挂载到任意路由的组件选项 |
| 渲染前提 | 父路由必须渲染<Outlet> | 无此要求,自动插入渲染 |
| 布局支持 | 无法使用布局(layout) | 可与布局(layout)配合使用 |
| 路径匹配 | 宽松:/post/1/2/3会匹配NotFoundRoute | 严格:声明了/post/$postId时访问/post/1/2/3会抛 not-found 错误 |
| Outlet 渲染 | 支持 | 不支持 |
迁移只需几步修改。以src/router.tsx为例:
import { createRouter } from '@tanstack/react-router' import { routeTree } from './routeTree.gen.' - import { notFoundRoute } from './notFoundRoute' // [!code --] export const router = createRouter({ routeTree, - notFoundRoute // [!code --] }) // routes/__root.tsx import { createRootRoute } from '@tanstack/react-router' export const Route = createRootRoute({ // ... + notFoundComponent: () => { // [!code ++] + return <p>Not found!</p> // [!code ++] + } // [!code ++] })迁移中的关键变化:
- 在根路由上添加
notFoundComponent用于全局 not-found 处理;也可以在路由树中的任意其他路由上添加notFoundComponent,以处理该路由专属的 not-found 错误; - 移除传给
createRouter的notFoundRoute选项及其导入; - 牢记
notFoundComponent不支持渲染<Outlet />,原有依赖NotFoundRoute+<Outlet>的布局结构需要调整为"在父路由的component中保留<Outlet />,同时把 404 展示逻辑迁入notFoundComponent"。
【免费下载链接】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),仅供参考