关于 not-found-and-errors
面向 TanStack Router 开发者的技能,讲解未找到状态、路由错误边界和路由遮罩,帮助配置全局或逐路由处理并避免常见的数据与重试问题。
主要功能
- 01全局和逐路由 404 处理
- 02使用 notFound() 处理缺失资源
- 03逐路由及全局错误组件
- 04模糊与根级未找到模式
- 05路由遮罩及重新加载行为
适用场景
- 01为未匹配路径显示 404 页面
- 02处理 loader 中缺失的记录
- 03修复错误后重试 loader
- 04通过遮罩 URL 展示模态路由
安装
复制下面的命令,在终端中运行。
npx skills add https:/ / github.com/ tanstack/ router --skill not-found-and-errors安装后在 Claude Code 中输入/not-found-and-errors确认已加载。
详细介绍
未找到和错误
TanStack Router 会处理两类“未找到”情况:未匹配的 URL 路径(自动处理)和缺失的资源,例如不存在的帖子(通过 notFound() 手动处理)。错误边界通过各路由的 errorComponent 配置。
CRITICAL:请勿使用已弃用的
NotFoundRoute。存在该配置时,notFound()和notFoundComponent将无法生效。请将其移除并改用notFoundComponent。 CRITICAL:在notFoundComponent中,useLoaderData可能是 undefined。请改用useParams、useSearch或useRouteContext。
未找到处理
全局 404:在根路由上设置 notFoundComponent
// src/routes/__root.tsx
import { createRootRoute, Outlet, Link } from '@tanstack/react-router'
export const Route = createRootRoute({
component: () => <Outlet />,
notFoundComponent: () => {
return (
<div>
<h1>404 — Page Not Found</h1>
<Link to="/">Go Home</Link>
</div>
)
},
})
整个路由器的默认组件:defaultNotFoundComponent
// src/router.tsx
import { createRouter } from '@tanstack/react-router'
import { routeTree } from './routeTree.gen'
const router = createRouter({
routeTree,
defaultNotFoundComponent: () => {
return (
<div>
<p>Not found!</p>
<Link to="/">Go home</Link>
</div>
)
},
})
按路由处理 404:通过 notFound() 处理缺失的资源
当资源不存在时,在 loader 或 beforeLoad 中抛出 notFound()。它的工作方式与 redirect() 类似:抛出它即可触发未找到边界。
// 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 <p>Post "{postId}" not found</p>
},
})
function PostComponent() {
const { post } = Route.useLoaderData()
return <h1>{post.title}</h1>
}
使用 notFound({ routeId }) 指定特定路由
你可以指定由某个父路由处理未找到错误:
// src/routes/_layout/posts.$postId.tsx
import { createFileRoute, notFound } from '@tanstack/react-router'
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 指定根路由
import { 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 }
},
})
notFoundMode:模糊模式与根路由模式
fuzzy(默认)
路由器会查找最近的、包含子路由且设置了 notFoundComponent 的父路由。这样可以尽可能保留父级布局。
对于路由 __root__ → posts → $postId,访问 /posts/1/edit 时:
- 渲染
<Root> - 渲染
<Posts> - 渲染
<Posts.notFoundComponent>(最近的、包含子路由且设置了notFoundComponent的父路由)
root
无论匹配情况如何,所有未找到错误都会交给根路由的 notFoundComponent 处理:
const router = createRouter({
routeTree,
notFoundMode: 'root',
})
错误处理
按路由设置 errorComponent
errorComponent 会接收 error、info 和 reset 属性。对于 loader 错误,请使用 router.invalidate() 重新运行 loader——它会自动重置错误边界。
// 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
info?: { componentStack: string }
reset: () => void
}) {
const router = useRouter()
return (
<div>
<p>Error: {error instanceof Error ? error.message : String(error)}</p>
<button
onClick={() => {
// Invalidate re-runs the loader and resets the error boundary
router.invalidate()
}}
>
Retry
</button>
</div>
)
}
function PostComponent() {
const data = Route.useLoaderData()
return <h1>{data.title}</h1>
}
整个路由器的默认错误组件
const 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>
)
},
})
notFoundComponent 中的数据
由于 loader 可能尚未完成,notFoundComponent 无法可靠地使用 useLoaderData。可安全使用的钩子:
notFoundComponent: ({ 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 <p>Item {params.id} not found</p>
}
要传递部分数据,请使用 notFound() 的 data 选项:
loader: async ({ params }) => {
const partialData = await getPartialData(params.id)
if (!partialData.fullResource) {
throw notFound({ data: { name: partialData.name } })
}
return partialData
},
notFoundComponent: ({ data }) => {
// data is typed as unknown — validate it
const info = data as { name: string } | undefined
return <p>{info?.name ?? 'Resource'} not found</p>
},
路由遮罩
路由遮罩会在浏览器地址栏中显示与实际渲染路由不同的 URL。遮罩数据存储在 location.state 中;分享 URL 或在新标签页中打开 URL 时,该数据会丢失。
在 <Link> 上以命令式方式设置遮罩
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>
)
}
使用 useNavigate 以命令式方式设置遮罩
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 以声明式方式设置遮罩
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],
})
重新加载时取消遮罩
默认情况下,页面在本地重新加载后会保留遮罩。要在重新加载时取消遮罩:
// Per-mask
const mask = createRouteMask({
routeTree,
from: '/photos/$photoId/modal',
to: '/photos/$photoId',
params: (prev) => ({ photoId: prev.photoId }),
unmaskOnReload: true,
})
// Per-link
<Link
to="/photos/$photoId/modal"
params={{ photoId }}
mask={{ to: '/photos/$photoId', params: { photoId } }}
unmaskOnReload
>
Open Photo
</Link>
// Router-wide default
const router = createRouter({
routeTree,
unmaskOnReload: true,
})
常见错误
1. HIGH:使用已弃用的 NotFoundRoute
// WRONG — NotFoundRoute blocks notFound() and notFoundComponent from working
import { NotFoundRoute } from '@tanstack/react-router'
const notFoundRoute = new NotFoundRoute({ component: () => <p>404</p> })
const router = createRouter({ routeTree, notFoundRoute })
// CORRECT — use notFoundComponent on root route
export const Route = createRootRoute({
component: () => <Outlet />,
notFoundComponent: () => <p>404</p>,
})
2. MEDIUM:以为能在 notFoundComponent 中使用 useLoaderData
// WRONG — loader may not have completed
notFoundComponent: () => {
const data = Route.useLoaderData() // may be undefined!
return <p>{data.title} not found</p>
}
// CORRECT — use safe hooks
notFoundComponent: () => {
const { postId } = Route.useParams()
return <p>Post {postId} not found</p>
}
3. MEDIUM:叶路由无法处理未找到错误
只有包含子路由(因此有 <Outlet>)的路由才能渲染 notFoundComponent。叶路由(不包含子路由的路由)永远不会捕获未找到错误——错误会向上冒泡到最近的、包含子路由的父路由。
// This route has NO children — notFoundComponent here will not catch
// unmatched child paths (there are no child paths to unmatch)
export const Route = createFileRoute('/posts/$postId')({
// notFoundComponent here only works for notFound() thrown in THIS route's loader
// It does NOT catch path-based not-founds
notFoundComponent: () => <p>Not found</p>,
})
4. MEDIUM:以为分享后遮罩 URL 仍会保留
遮罩数据存储在 location.state(浏览器历史记录)中。复制、分享或在新标签页中打开遮罩 URL 时,遮罩数据会丢失。浏览器会直接导航到可见的(经过遮罩的)URL。
5. HIGH(跨技能):只使用 reset(),而不使用 router.invalidate()
// WRONG — reset() clears the error boundary but does NOT re-run the loader
function ErrorFallback({
error,
reset,
}: {
error: unknown
reset: () => void
}) {
return <button onClick={reset}>Retry</button>
}
// CORRECT — invalidate re-runs loaders and resets the error boundary
function ErrorFallback({ error }: { error: unknown; reset: () => void }) {
const router = useRouter()
return (
<button
onClick={() => {
router.invalidate()
}}
>
Retry
</button>
)
}
交叉参考
- router-core/data-loading — 在 loader 中抛出
notFound()会影响错误边界和 loader 数据的可用性。errorComponent的重试需要使用router.invalidate()。 - router-core/type-safety —
notFoundComponent的数据类型为unknown;使用前请进行验证。