mcpField
N
not-found-and-errors

not-found-and-errors

github.com/tanstack开发15.3k199 次安装4 天前更新
代码仓库

关于 not-found-and-errors

面向 TanStack Router 开发者的技能,讲解未找到状态、路由错误边界和路由遮罩,帮助配置全局或逐路由处理并避免常见的数据与重试问题。

主要功能

  1. 01全局和逐路由 404 处理
  2. 02使用 notFound() 处理缺失资源
  3. 03逐路由及全局错误组件
  4. 04模糊与根级未找到模式
  5. 05路由遮罩及重新加载行为

适用场景

  1. 01为未匹配路径显示 404 页面
  2. 02处理 loader 中缺失的记录
  3. 03修复错误后重试 loader
  4. 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;使用前请进行验证。

评论

支持 Markdown