not-found-and-errors について
TanStack Router 開発者向けのスキルです。未検出状態、ルートのエラーバウンダリー、ルートマスキングを扱い、全体またはルート単位の設定とデータ取得・再試行時の注意点を説明します。
主な機能
- 01全体およびルート単位の 404 処理
- 02notFound() によるリソース欠落の処理
- 03ルート別および全体のエラーコンポーネント
- 04ファジーおよびルート起点の未検出モード
- 05ルートマスキングと再読み込み時の動作
活用シーン
- 01未一致パスに 404 ページを表示
- 02loader で欠落レコードを処理
- 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 は「見つからない」ケースを 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型です。使用前に検証してください。