tanstack-router について
TanStack Router は、React SPA 向けの型安全なルーティングライブラリです。ファイルベースのルートやルート単位のデータ読み込みが必要な開発者向けで、TanStack Query 連携、検索パラメーター検証、仮想ルート設定にも対応します。
主な機能
- 01型安全なファイルベースルーティング
- 02ルート単位のデータ読み込み
- 03TanStack Query による事前取得とキャッシュ
- 04URL 検索パラメーターの検証
- 05ルート単位のエラー・未検出処理
- 06仮想ファイルルート対応
活用シーン
- 01React SPA のファイルベースルート設定
- 02ページ描画前のルートデータ読み込み
- 03URL 検索パラメーターの検証と処理
- 04複雑なネストルートやレイアウトの構成
インストール
コマンドをコピーしてターミナルで実行します。
npx skills add https:/ / github.com/ jezweb/ claude-skills --skill tanstack-routerインストール後、Claude Code で/tanstack-routerを入力して読み込まれたか確認します。
同梱ファイル
SKILL.md と一緒にインストールされる参考資料やスクリプト。
README.md.claude-plugin/plugin.jsonreferences/common-errors.mdrules/tanstack-router.mdtemplates/package.jsonroute-examples.tsxvite.config.ts詳細
TanStack Router
ルート単位のデータ読み込みと TanStack Query との統合に対応した、React SPA 向けの型安全なファイルベースルーティング
クイックスタート
最終更新日:2026-01-09 バージョン:@tanstack/[email protected]
npm install @tanstack/react-router @tanstack/router-devtools
npm install -D @tanstack/router-plugin
# Optional: Zod validation adapter
npm install @tanstack/zod-adapter zod
Vite の設定(TanStackRouterVite は react() より前に指定する必要があります):
// vite.config.ts
import { TanStackRouterVite } from '@tanstack/router-plugin/vite'
export default defineConfig({
plugins: [TanStackRouterVite(), react()], // Order matters!
})
ファイル構成:
src/routes/
├── __root.tsx → createRootRoute() with <Outlet />
├── index.tsx → createFileRoute('/')
└── posts.$postId.tsx → createFileRoute('/posts/$postId')
アプリのセットアップ:
import { createRouter, RouterProvider } from '@tanstack/react-router'
import { routeTree } from './routeTree.gen' // Auto-generated by plugin
const router = createRouter({ routeTree })
<RouterProvider router={router} />
コアパターン
型安全なナビゲーション(ルートは自動補完され、パラメーターは型付けされます):
<Link to="/posts/$postId" params={{ postId: '123' }} />
<Link to="/invalid" /> // ❌ TypeScript error
ルートローダー(レンダリング前のデータ取得):
export const Route = createFileRoute('/posts/$postId')({
loader: async ({ params }) => ({ post: await fetchPost(params.postId) }),
component: ({ useLoaderData }) => {
const { post } = useLoaderData() // Fully typed!
return <h1>{post.title}</h1>
},
})
TanStack Query との統合(プリフェッチ+キャッシュ):
const postOpts = (id: string) => queryOptions({
queryKey: ['posts', id],
queryFn: () => fetchPost(id),
})
export const Route = createFileRoute('/posts/$postId')({
loader: ({ context: { queryClient }, params }) =>
queryClient.ensureQueryData(postOpts(params.postId)),
component: () => {
const { postId } = Route.useParams()
const { data } = useQuery(postOpts(postId))
return <h1>{data.title}</h1>
},
})
仮想ファイルルート(v1.140+)
ファイルベースの規約が要件に合わない場合に、プログラムでルートを設定できます。
インストール:npm install @tanstack/virtual-file-routes
Vite の設定:
import { tanstackRouter } from '@tanstack/router-plugin/vite'
export default defineConfig({
plugins: [
tanstackRouter({
target: 'react',
virtualRouteConfig: './routes.ts', // Point to your routes file
}),
react(),
],
})
routes.ts(プログラムでルートを定義):
import { rootRoute, route, index, layout, physical } from '@tanstack/virtual-file-routes'
export const routes = rootRoute('root.tsx', [
index('home.tsx'),
route('/posts', 'posts/posts.tsx', [
index('posts/posts-home.tsx'),
route('$postId', 'posts/posts-detail.tsx'),
]),
layout('first', 'layout/first-layout.tsx', [
route('/nested', 'nested.tsx'),
]),
physical('/classic', 'file-based-subtree'), // Mix with file-based
])
ユースケース:カスタムのルート構成、ファイルベースとコードベースの併用、複雑なネストレイアウト。
検索パラメーターのバリデーション(Zod アダプター)
実行時のバリデーションに対応した、型安全な URL 検索パラメーター:
基本パターン(インラインバリデーション):
import { z } from 'zod'
export const Route = createFileRoute('/products')({
validateSearch: (search) => z.object({
page: z.number().catch(1),
filter: z.string().catch(''),
sort: z.enum(['newest', 'oldest', 'price']).catch('newest'),
}).parse(search),
})
推奨パターン(フォールバック付きの Zod アダプター):
import { zodValidator, fallback } from '@tanstack/zod-adapter'
import { z } from 'zod'
const searchSchema = z.object({
query: z.string().min(1).max(100),
page: fallback(z.number().int().positive(), 1),
sortBy: z.enum(['name', 'date', 'relevance']).optional(),
})
export const Route = createFileRoute('/search')({
validateSearch: zodValidator(searchSchema),
// Type-safe: Route.useSearch() returns typed params
})
.default() より .catch() を使う理由:形式が不正なパラメーターを静かに修正するには、.catch() を使います。バリデーションエラーを表示するには、.default() と errorComponent を使います。
エラーバウンダリー
型付きのエラーコンポーネントを使って、ルート単位でエラーを処理します。
ルート単位のエラー処理:
export const Route = createFileRoute('/posts/$postId')({
loader: async ({ params }) => {
const post = await fetchPost(params.postId)
if (!post) throw new Error('Post not found')
return { post }
},
errorComponent: ({ error, reset }) => (
<div>
<p>Error: {error.message}</p>
<button onClick={reset}>Retry</button>
</div>
),
})
デフォルトのエラーコンポーネント(グローバルフォールバック):
const router = createRouter({
routeTree,
defaultErrorComponent: ({ error }) => (
<div className="error-page">
<h1>Something went wrong</h1>
<p>{error.message}</p>
</div>
),
})
Not Found の処理:
export const Route = createFileRoute('/posts/$postId')({
notFoundComponent: () => <div>Post not found</div>,
})
beforeLoad による認証
ルートの読み込み前に保護します(保護対象のコンテンツが一瞬表示されることはありません)。
単一ルートの保護:
import { redirect } from '@tanstack/react-router'
export const Route = createFileRoute('/dashboard')({
beforeLoad: async ({ context }) => {
if (!context.auth.isAuthenticated) {
throw redirect({
to: '/login',
search: { redirect: location.pathname }, // Save for post-login
})
}
},
})
複数ルートの保護(レイアウトルートパターン):
// routes/(authenticated)/route.tsx - protects all children
export const Route = createFileRoute('/(authenticated)')({
beforeLoad: async ({ context }) => {
if (!context.auth.isAuthenticated) {
throw redirect({ to: '/login' })
}
},
})
認証コンテキストの受け渡し(React フックから):
// main.tsx - pass auth state to router
function App() {
const auth = useAuth() // Your auth hook
return (
<RouterProvider
router={router}
context={{ auth }} // Available in beforeLoad
/>
)
}
既知の問題の予防
このスキルは、文書化されている 20 件の問題を予防します。
Issue #1:Devtools の依存関係の解決
- エラー:
@tanstack/router-devtools-coreが見つからず、ビルドに失敗する - 修正方法:
npm install @tanstack/router-devtools
Issue #2:Vite プラグインの順序(重大)
- エラー:ルートが自動生成されず、
routeTree.gen.tsが見つからない - 修正方法:plugins 配列で TanStackRouterVite を react() より前に指定する必要があります
- 理由:プラグインは React のコンパイル前にルートファイルを処理します
Issue #3:型登録がない
- エラー:
<Link to="...">が型付けされず、自動補完もされない - 修正方法:型を登録するため、main.tsx で
./routeTree.genからrouteTreeをインポートします
Issue #4:ローダーが実行されない
- エラー:ナビゲーション時にローダー関数が呼び出されない
- 修正方法:ルートで
Route定数がエクスポートされていることを確認します:export const Route = createFileRoute('/path')({ loader: ... })
Issue #5:TanStack Form によるメモリーリーク(修正済み)
- エラー:TanStack Form と Router の併用時に、本番環境でクラッシュする
- 出典:GitHub Issue #5734(2026年1月5日にクローズ)
- 解決方法:最新バージョンの @tanstack/form と @tanstack/react-start で修正されています。両方のパッケージを更新すると解決します。
Issue #6:仮想ルートのインデックス/レイアウトの競合
- エラー:仮想ルーティングで
physical()を使うと、route.tsx と index.tsx が競合する - 出典:GitHub Issue #5421
- 修正方法:代わりにパスレスルートを使います:
_layout.tsxと_layout.index.tsx
Issue #7:検索パラメーターの型推論
- エラー:
zodSearchValidatorで型推論が機能しない - 出典:GitHub Issue #3100(v1.81.5 以降のリグレッション)
- 修正方法:代わりに
@tanstack/zod-adapterのzodValidatorを使います
Issue #8:再読み込み時の TanStack Start バリデーター
- エラー:TanStack Start でページを再読み込みすると、
validateSearchが機能しない - 出典:GitHub Issue #3711
- 注記:クライアントサイドのナビゲーションでは機能しますが、ページを直接読み込むと失敗します
Issue #9: サーバー関数の検証エラーで構造が失われる
エラー: inputValidator の Zod エラーが文字列化され、クライアント側で構造が失われます
出典: GitHub Issue #6428
発生する理由: TanStack Start のサーバー関数のエラーシリアライズにより、Zod の issues 配列が error.message 内の JSON 文字列に変換され、手動で解析しないと利用できなくなります。
予防策:
// Server function with input validation
export const myFn = createServerFn({ method: 'POST' })
.inputValidator(z.object({
name: z.string().min(2),
age: z.number().min(18),
}))
.handler(async ({ data }) => data)
// Client: Workaround to parse stringified issues
try {
await mutation.mutate({ data: invalidData })
} catch (error) {
if (error.message.startsWith('[')) {
const issues = JSON.parse(error.message)
// Now can use structured error data
issues.forEach(issue => {
console.log(issue.path, issue.message)
})
}
}
公式ステータス: 既知の問題です。修正のための PR を追跡中です。
Issue #10: useParams({ strict: false }) が未解析の値を返す
エラー: パラメーターは解析済みとして型付けされていますが、ナビゲーション後に文字列として返されます
出典: GitHub Issue #6385
発生する理由: v1.147.3 以降、strict: false を使用すると match.params が解析されなくなります。初回のレンダリングは正しく動作しますが、ナビゲーション後の値は解析済みの型ではなく文字列として保存されます。
予防策:
// Route with param parsing
export const Route = createFileRoute('/posts/$postId')({
params: {
parse: (params) => ({
postId: z.coerce.number().parse(params.postId),
}),
},
})
// Component: Use strict mode (default) for parsed params
function Component() {
const { postId } = useParams() // ✓ Parsed as number
// const { postId } = useParams({ strict: false }) // ✗ String!
// Or manually parse when using strict: false
const params = useParams({ strict: false })
const postId = Number(params.postId)
}
公式ステータス: 既知の問題です。回避策が必要です。
Issue #11: パスレスルートの notFoundComponent がレンダリングされない
エラー: パスレスレイアウトルートの notFoundComponent が無視されます
出典: GitHub Issue #6351, GitHub Issue #4065
発生する理由: パスレスルート(例: routes/(authenticated)/route.tsx)では、その notFoundComponent がレンダリングされません。代わりに、ルーター設定の defaultNotFoundComponent が呼び出されます。この問題は 2025 年 4 月以降発生しています。
予防策:
// ✗ Doesn't work: notFoundComponent on pathless layout
export const Route = createFileRoute('/(authenticated)')({
beforeLoad: ({ context }) => {
if (!context.auth) throw redirect({ to: '/login' })
},
notFoundComponent: () => <div>Protected 404</div>, // Not rendered!
})
// ✓ Works: Define on child routes instead
export const Route = createFileRoute('/(authenticated)/dashboard')({
notFoundComponent: () => <div>Protected 404</div>,
})
公式ステータス: 既知の問題です。回避策が必要です。
Issue #12: 中断されたローダーが未定義のエラーとともに errorComponent をレンダリングする
エラー: 短時間に続けてナビゲーションすると、前のローダーが中断され、エラーが undefined のまま errorComponent がレンダリングされます
出典: GitHub Issue #6388
発生する理由: PR #4570 の後に導入された副作用です。ユーザーが短時間に続けてナビゲーションすると(例: リスト項目を次々にクリックする場合)、中断されたフェッチリクエストによって、アボートエラーが渡されないまま errorComponent が呼び出されます。
予防策:
export const Route = createFileRoute('/posts/$postId')({
loader: async ({ params, abortController }) => {
await fetch(`/api/posts/${params.postId}`, {
signal: abortController.signal,
})
},
errorComponent: ({ error, reset }) => {
// Check for undefined error (aborted request)
if (!error) {
return null // Or show loading state
}
return <div>Error: {error.message}</div>
},
})
公式ステータス: 既知の問題です。回避策が必要です。
Issue #13: Vitest で Cannot Read Properties of Null (useState) が発生する
エラー: Vitest でテストを実行すると Cannot read properties of null (reading 'useState') が発生します
出典: GitHub Issue #6262, PR #6074
発生する理由: TanStack Start の tanstackStart() プラグインが、Vitest による React フックのレンダリングと競合します。これは既知の重複した問題で、対応する PR が進行中です。
予防策:
// Temporary workaround: Comment out tanstackStart() for tests
// vite.config.ts
export default defineConfig({
plugins: [
// tanstackStart(), // Disable for tests
react(),
],
test: { environment: 'jsdom' },
})
公式ステータス: 修正に向けて PR #6074 が進行中です。
Issue #14: ストリーミング SSR のローダーでエラーをスローすると開発サーバーがクラッシュする
エラー: ルートローダーが await せずにエラーをスローすると(await ではなく void を使用すると)、開発サーバーがクラッシュします
出典: GitHub Issue #6200
発生する理由: SSR のストリーミングモードでは、await されていない Promise の拒否を処理できません。エラーがローダーのコンテキストから外に伝播し、ワーカープロセスがクラッシュします。
予防策:
// ✗ Wrong: void + throw crashes dev server
export const Route = createFileRoute('/posts')({
loader: async () => {
void fetch('/api/posts').then(r => {
throw new Error('boom') // Crashes!
})
},
})
// ✓ Correct: Always await or catch
export const Route = createFileRoute('/posts')({
loader: async () => {
try {
const data = await fetch('/api/posts')
return data
} catch (error) {
throw error // Caught by errorComponent
}
},
})
公式ステータス: 既知の問題です。回避策が必要です。
Issue #15: フィルターの結果がゼロ件の場合、プリレンダリングが無期限に停止する
エラー: prerender.filter がルートを 1 つも返さないと、ビルド手順が停止します
出典: GitHub Issue #6425
発生する理由: TanStack Start のプリレンダリングは、ルートのセットが空の場合を適切に処理できず、存在しないルートを待ち続けます。
予防策:
// ✗ Wrong: Empty filter causes hang
tanstackStart({
prerender: {
enabled: true,
filter: (route) => false, // No routes → hangs!
},
})
// ✓ Correct: Ensure at least one route or disable
tanstackStart({
prerender: {
enabled: true,
filter: (route) => route.path === '/' || route.path.startsWith('/posts'),
},
})
// Or temporarily disable
tanstackStart({
prerender: { enabled: false },
})
公式ステータス: 既知の問題です。回避策が必要です。
Issue #16:Docker でプリレンダリングが機能しない
エラー:プリレンダリングのステップで「Unable to connect」と表示され、Docker でのビルドに失敗する ソース:GitHub Issue #6275、PR #6305 発生する理由:プリレンダリングに使用する Vite のプレビューサーバーに、Docker 環境からアクセスできません。
予防策:
// vite.config.ts - Make preview server accessible in Docker
export default defineConfig({
preview: {
host: true, // Bind to 0.0.0.0 instead of localhost
},
plugins: [
devtools(),
// nitro({ preset: "bun" }), // Remove temporarily if issues persist
tanstackStart(),
react(),
],
})
公式ステータス:PR #6305 は進行中です
Issue #17:ローダーの完了前にルートの Head 関数が実行される
エラー:head() が loader() より先に実行されると、不完全なデータでメタタグが生成される
ソース:GitHub Issue #6221
発生する理由:ルートの loader() が完了する前に head() 関数が実行される場合があり、その結果、メタタグにプレースホルダーや未定義のデータが使用されます。
予防策:
// ✗ Wrong: loaderData may not be available yet
export const Route = createFileRoute('/posts/$postId')({
loader: async ({ params }) => {
const post = await fetchPost(params.postId)
return { post }
},
head: ({ loaderData }) => ({
meta: [
{ title: loaderData.post.title }, // May be undefined!
],
}),
})
// ✓ Correct: Explicitly await if needed
export const Route = createFileRoute('/posts/$postId')({
loader: async ({ params }) => {
const post = await fetchPost(params.postId)
return { post }
},
head: async ({ loaderData }) => {
await loaderData // Ensure loaded
return {
meta: [{ title: loaderData.post.title }],
}
},
})
公式ステータス:既知の問題であり、回避策が必要です
Issue #18:仮想ルートでは手動の遅延読み込みがサポートされない(コミュニティ提供)
エラー:仮想ルートで createLazyFileRoute が自動的に createFileRoute に置き換えられる
ソース:GitHub Issue #6396
発生する理由:仮想ファイルルートは、自動コード分割のみを目的として設計されています。手動の遅延ルートはサポートされておらず、プラグインによって警告なく置き換えられます。
予防策:
// Virtual routes: Use automatic code splitting
// vite.config.ts
tanstackRouter({
target: 'react',
virtualRouteConfig: './routes.ts',
autoCodeSplitting: true, // Use automatic splitting
})
// Don't use createLazyFileRoute in virtual routes
// It will be replaced with createFileRoute automatically
公式ステータス:仕様どおり(文書化された動作)
Issue #19:NavigateOptions の型安全性に一貫性がない(コミュニティ提供)
エラー:NavigateOptions 型では、useNavigate() のように必須パラメーターが強制されない
ソース:TkDodo のブログ:The Beauty of TanStack Router
発生する理由:実行時フックと型ヘルパーで型定義が異なります。NavigateOptions の制約はより緩やかです。
予防策:
// ✗ Wrong: NavigateOptions doesn't catch missing params
const options: NavigateOptions = {
to: '/posts/$postId', // No TS error, but params required!
}
// ✓ Correct: Use useNavigate() return type
const navigate = useNavigate()
type NavigateFn = typeof navigate
// Now type-safe across all usages
検証済み:TanStack Query のメンテナーによる分析と照合済み
Issue #20:ルートパスの先頭のスラッシュが抜けている(コミュニティ提供)
エラー:先頭のスラッシュなしでパスを定義すると、ルートのマッチに失敗する
ソース:公式デバッグガイド
発生する理由:初心者によくある間違いです。'/about' の代わりに 'about' を使用すると、ルートのマッチに失敗します。
予防策:
// ✗ Wrong: Missing leading slash
export const Route = createFileRoute('about')({ /* ... */ })
// ✓ Correct: Always start with /
export const Route = createFileRoute('/about')({ /* ... */ })
検証済み:公式ドキュメントで確認済み。よくあるデバッグ上の問題です
Cloudflare Workers との統合
Vite の設定(@cloudflare/vite-plugin を追加):
import { cloudflare } from '@cloudflare/vite-plugin'
export default defineConfig({
plugins: [TanStackRouterVite(), react(), cloudflare()],
})
API ルートのパターン(Workers バックエンドからフェッチ):
// Worker: functions/api/posts.ts
export async function onRequestGet({ env }) {
const { results } = await env.DB.prepare('SELECT * FROM posts').all()
return Response.json(results)
}
// Router: src/routes/posts.tsx
export const Route = createFileRoute('/posts')({
loader: async () => fetch('/api/posts').then(r => r.json()),
})
関連スキル:tanstack-query(データ取得)、react-hook-form-zod(フォーム検証)、cloudflare-worker-base(API バックエンド)、tailwind-v4-shadcn(UI)
関連パッケージ:@tanstack/zod-adapter(検索の検証)、@tanstack/virtual-file-routes(プログラムによるルート定義)
最終検証日:2026-01-20 | スキルバージョン:2.0.0 | 変更内容:コミュニティ調査に基づく新たな問題を12件追加(inputValidator の構造消失、useParams の解析バグ、pathless notFoundComponent、loader の中断エラー、Vitest との競合、SSR ストリーミングのクラッシュ、Docker のプリレンダリング問題、head/loader の実行タイミング、仮想ルートの遅延読み込み制限、NavigateOptions の型の不整合、先頭のスラッシュのよくある間違い)。エラー予防の項目を8件から、文書化された20件に拡充しました。