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. 02notFound() によるリソース欠落の処理
  3. 03ルート別および全体のエラーコンポーネント
  4. 04ファジーおよびルート起点の未検出モード
  5. 05ルートマスキングと再読み込み時の動作

活用シーン

  1. 01未一致パスに 404 ページを表示
  2. 02loader で欠落レコードを処理
  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 は「見つからない」ケースを 2 種類扱います。マッチする URL パスがない場合(自動)と、存在しない投稿などのリソースがない場合(notFound() を使った手動)です。エラーバウンダリーは、ルートごとに errorComponent で設定します。

CRITICAL: 非推奨の NotFoundRoute は使用しないでください。これが存在すると、notFound() と notFoundComponent は機能しません。削除して、代わりに notFoundComponent を使用してください。 CRITICAL: notFoundComponent 内では useLoaderData が未定義の場合があります。代わりに 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 の各プロパティを受け取ります。ローダーのエラーを再試行するには、router.invalidate() を使用してローダーを再実行します。これにより、エラーバウンダリーも自動的にリセットされます。

// 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 内のデータ

ローダーが完了していない場合があるため、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 を共有したり、新しいタブで開いたりすると失われます。

<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(スキル横断):router.invalidate() の代わりに reset() だけを使用する
// 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 — ローダーでスローされた notFound() は、エラーバウンダリーとローダーデータの利用可否に影響します。errorComponent の再試行には router.invalidate() が必要です。
  • router-core/type-safety — notFoundComponent のデータは unknown 型です。使用前に検証してください。

コメント