mcpField
T
tanstack-router

tanstack-router

github.com/jezweb開発1.1k799 回インストール3 週間前に更新
リポジトリ

tanstack-router について

TanStack Router は、React SPA 向けの型安全なルーティングライブラリです。ファイルベースのルートやルート単位のデータ読み込みが必要な開発者向けで、TanStack Query 連携、検索パラメーター検証、仮想ルート設定にも対応します。

主な機能

  1. 01型安全なファイルベースルーティング
  2. 02ルート単位のデータ読み込み
  3. 03TanStack Query による事前取得とキャッシュ
  4. 04URL 検索パラメーターの検証
  5. 05ルート単位のエラー・未検出処理
  6. 06仮想ファイルルート対応

活用シーン

  1. 01React SPA のファイルベースルート設定
  2. 02ページ描画前のルートデータ読み込み
  3. 03URL 検索パラメーターの検証と処理
  4. 04複雑なネストルートやレイアウトの構成

インストール

コマンドをコピーしてターミナルで実行します。

$npx skills add https://github.com/jezweb/claude-skills --skill tanstack-router

インストール後、Claude Code で/tanstack-routerを入力して読み込まれたか確認します。

同梱ファイル

SKILL.md と一緒にインストールされる参考資料やスクリプト。

README.md
.claude-plugin/
plugin.json
references/
common-errors.md
rules/
tanstack-router.md
templates/
package.json
route-examples.tsx
vite.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件に拡充しました。

コメント