Agent Skills by ALSEL
Anthropic Claudeソフトウェア開発⭐ リポ 0品質スコア 50/100

tanstack-router

ReactおよびSolidアプリケーション向けの型安全なルーティングを提供し、検索パラメータの一流サポート、データローディング、そしてReactエコシステムとのシームレスな統合を実現します。

description の原文を見る

Type-safe routing for React and Solid applications with first-class search params, data loading, and seamless integration with the React ecosystem.

SKILL.md 本文

概要

TanStack Router は React (および Solid) アプリケーション向けの完全な型安全なルーターです。ファイルベースのルーティング、ファーストクラスの検索パラメータ管理、組み込みデータロード、コード分割、深い TypeScript 統合を提供します。TanStack Start (フルスタックフレームワーク) のルーティング基盤として機能します。

パッケージ: @tanstack/react-router CLI: @tanstack/router-cli または @tanstack/router-plugin (Vite/Rspack/Webpack) Devtools: @tanstack/react-router-devtools

インストール

npm install @tanstack/react-router
# Vite でファイルベースのルーティングを使用する場合:
npm install -D @tanstack/router-plugin
# またはスタンドアロン CLI:
npm install -D @tanstack/router-cli

コアコンセプト

ルートツリー

ルートはツリー構造で整理されます。ルートルートはトップレベルレイアウトで、子ルートはその下にネストされます。

import { createRootRoute, createRoute, createRouter } from '@tanstack/react-router'

const rootRoute = createRootRoute({
  component: RootLayout,
})

const indexRoute = createRoute({
  getParentRoute: () => rootRoute,
  path: '/',
  component: HomePage,
})

const aboutRoute = createRoute({
  getParentRoute: () => rootRoute,
  path: '/about',
  component: AboutPage,
})

const routeTree = rootRoute.addChildren([indexRoute, aboutRoute])
const router = createRouter({ routeTree })

ファイルベースのルーティング

ファイルベースのルーティングは、ファイル構造からルートツリーを自動生成します。Vite プラグインで設定します:

// vite.config.ts
import { defineConfig } from 'vite'
import { TanStackRouterVite } from '@tanstack/router-plugin/vite'

export default defineConfig({
  plugins: [
    TanStackRouterVite(),
    // ... 他のプラグイン
  ],
})

ファイル命名規則

ファイルパターンルートタイプ例のパス
__root.tsxルートレイアウトN/A (全てをラップ)
index.tsxインデックスルート/
about.tsx静的ルート/about
$postId.tsx動的パラメータ/posts/$postId
posts.tsxレイアウトルート/posts/* (レイアウト)
posts/index.tsxネストされたインデックス/posts
posts/$postId.tsxネストされた動的/posts/123
posts_.$postId.tsxパスレスレイアウト/posts/123 (異なるレイアウト)
_layout.tsxパスレスレイアウトN/A (ルートをグループ化)
_layout/dashboard.tsxグループ化されたルート/dashboard
$.tsxSplat/キャッチオール/*
posts.$postId.edit.tsxドット記法/posts/123/edit

特別なプレフィックス

  • _ プレフィックス: パスレスルート (URL セグメントなしのレイアウトグループ)
  • $ プレフィックス: 動的パスパラメータ
  • (folder) 括弧: ルートグループ (組織用で、URL に影響なし)

ルート設定

各ルートは以下を定義できます:

// routes/posts.$postId.tsx
import { createFileRoute } from '@tanstack/react-router'

export const Route = createFileRoute('/posts/$postId')({
  // パスパラメータの検証
  params: {
    parse: (params) => ({ postId: Number(params.postId) }),
    stringify: (params) => ({ postId: String(params.postId) }),
  },

  // 検索パラメータの検証
  validateSearch: (search: Record<string, unknown>) => {
    return {
      page: Number(search.page ?? 1),
      filter: (search.filter as string) || '',
    }
  },

  // データロード
  loader: async ({ params, context, abortController }) => {
    return fetchPost(params.postId)
  },

  // ローダー依存関係 (これらが変わるとローダーを再実行)
  loaderDeps: ({ search }) => ({ page: search.page }),

  // キャッシュされたローダーデータのステイル時間
  staleTime: 5_000,

  // プリロード
  preloadStaleTime: 30_000,

  // エラーコンポーネント
  errorComponent: PostErrorComponent,

  // 保留中/ロード中コンポーネント
  pendingComponent: PostLoadingComponent,

  // 404 コンポーネント
  notFoundComponent: PostNotFoundComponent,

  // 読み込み前フック (認証、リダイレクト)
  beforeLoad: async ({ context, location }) => {
    if (!context.auth.isAuthenticated) {
      throw redirect({
        to: '/login',
        search: { redirect: location.href },
      })
    }
  },

  // ヘッド/メタ管理
  head: () => ({
    meta: [{ title: 'Post Details' }],
  }),

  // コンポーネント
  component: PostComponent,
})

function PostComponent() {
  const { postId } = Route.useParams()
  const post = Route.useLoaderData()
  const { page, filter } = Route.useSearch()

  return <div>{post.title}</div>
}

データロード

ルートローダー

export const Route = createFileRoute('/posts')({
  loader: async ({ context }) => {
    // ルーターコンテキストにアクセス (例: queryClient)
    const posts = await context.queryClient.ensureQueryData({
      queryKey: ['posts'],
      queryFn: fetchPosts,
    })
    return { posts }
  },
  component: PostsComponent,
})

function PostsComponent() {
  const { posts } = Route.useLoaderData()
  // ...
}

ローダー依存関係

ローダーが再実行される時期を制御します:

export const Route = createFileRoute('/posts')({
  loaderDeps: ({ search: { page, filter } }) => ({ page, filter }),
  loader: async ({ deps: { page, filter } }) => {
    return fetchPosts({ page, filter })
  },
})

遅延データロード

重大でないデータをストリーミングします:

import { Await, defer } from '@tanstack/react-router'

export const Route = createFileRoute('/dashboard')({
  loader: async () => {
    const criticalData = await fetchCriticalData()
    const deferredData = defer(fetchSlowData())
    return { criticalData, deferredData }
  },
  component: DashboardComponent,
})

function DashboardComponent() {
  const { criticalData, deferredData } = Route.useLoaderData()

  return (
    <div>
      <CriticalSection data={criticalData} />
      <Suspense fallback={<Loading />}>
        <Await promise={deferredData}>
          {(data) => <SlowSection data={data} />}
        </Await>
      </Suspense>
    </div>
  )
}

コンテキストベースのデータロード

ルーターコンテキスト経由で共有の依存関係を提供します:

// コンテキスト付きルーターを作成
const router = createRouter({
  routeTree,
  context: {
    queryClient,
    auth: undefined!, // RouterProvider で提供されます
  },
})

// ルート/アプリコンポーネント内
function App() {
  const auth = useAuth()
  return <RouterProvider router={router} context={{ auth }} />
}

// ルート内
export const Route = createFileRoute('/protected')({
  beforeLoad: ({ context }) => {
    if (!context.auth.user) throw redirect({ to: '/login' })
  },
  loader: ({ context }) => {
    return context.queryClient.ensureQueryData(userQueryOptions())
  },
})

検索パラメータ

検証

import { z } from 'zod'

const postSearchSchema = z.object({
  page: z.number().default(1),
  filter: z.string().default(''),
  sort: z.enum(['date', 'title']).default('date'),
})

export const Route = createFileRoute('/posts')({
  validateSearch: postSearchSchema,
  // または手動検証:
  // validateSearch: (search) => postSearchSchema.parse(search),
})

検索パラメータの読み取り

function PostsComponent() {
  // ルートから
  const { page, filter, sort } = Route.useSearch()

  // または useSearch フックを使用した任意のコンポーネントから
  const search = useSearch({ from: '/posts' })
}

検索パラメータの更新

import { useNavigate } from '@tanstack/react-router'

function Pagination() {
  const navigate = useNavigate()
  const { page } = Route.useSearch()

  return (
    <button
      onClick={() =>
        navigate({
          search: (prev) => ({ ...prev, page: prev.page + 1 }),
        })
      }
    >
      Next Page
    </button>
  )
}

// または Link コンポーネント経由
<Link
  to="/posts"
  search={(prev) => ({ ...prev, page: 2 })}
>
  Page 2
</Link>

検索パラメータオプション

const router = createRouter({
  routeTree,
  // カスタムシリアライゼーション
  search: {
    strict: true, // 不明なパラメータを拒否
  },
  // デフォルト検索パラメータシリアライザー
  stringifySearch: defaultStringifySearch,
  parseSearch: defaultParseSearch,
})

ナビゲーション

Link コンポーネント

import { Link } from '@tanstack/react-router'

// 静的ルート
<Link to="/about">About</Link>

// パラメータを持つ動的ルート
<Link to="/posts/$postId" params={{ postId: '123' }}>
  Post 123
</Link>

// 検索パラメータ付き
<Link to="/posts" search={{ page: 2, filter: 'react' }}>
  Page 2
</Link>

// アクティブリンクのスタイリング
<Link
  to="/posts"
  activeProps={{ className: 'active' }}
  inactiveProps={{ className: 'inactive' }}
  activeOptions={{ exact: true }}
>
  Posts
</Link>

// プリロード
<Link to="/posts" preload="intent">Posts</Link>
<Link to="/dashboard" preload="viewport">Dashboard</Link>

// ハッシュ
<Link to="/docs" hash="api-reference">API Reference</Link>

プログラマティックナビゲーション

import { useNavigate, useRouter } from '@tanstack/react-router'

function MyComponent() {
  const navigate = useNavigate()
  const router = useRouter()

  // ルートにナビゲート
  navigate({ to: '/posts', search: { page: 1 } })

  // 置換でナビゲート
  navigate({ to: '/posts', replace: true })

  // 相対ナビゲーション
  navigate({ to: '.', search: (prev) => ({ ...prev, page: 2 }) })

  // 戻る/進む
  router.history.back()
  router.history.forward()

  // 現在のルートを無効化して再読み込み
  router.invalidate()
}

リダイレクト

import { redirect } from '@tanstack/react-router'

// beforeLoad または loader 内
throw redirect({
  to: '/login',
  search: { redirect: location.href },
  // オプションのステータスコード
  statusCode: 301, // 恒久的なリダイレクト (SSR)
})

ナビゲーションブロック

import { useBlocker } from '@tanstack/react-router'

function FormComponent() {
  const [isDirty, setIsDirty] = useState(false)

  useBlocker({
    shouldBlockFn: () => isDirty,
    withResolver: true, // 確認ダイアログを表示
  })

  // またはカスタム UI 付き
  const { proceed, reset, status } = useBlocker({
    shouldBlockFn: () => isDirty,
  })

  if (status === 'blocked') {
    return (
      <div>
        <p>Are you sure you want to leave?</p>
        <button onClick={proceed}>Leave</button>
        <button onClick={reset}>Stay</button>
      </div>
    )
  }
}

コード分割

自動 (ファイルベースのルーティング)

ファイルベースのルーティングを使用する場合、遅延ファイルを作成します:

routes/
  posts.tsx          # 重大: loader, beforeLoad, meta
  posts.lazy.tsx     # 遅延: component, pendingComponent, errorComponent
// posts.tsx (すぐに読み込まれます)
export const Route = createFileRoute('/posts')({
  loader: () => fetchPosts(),
})

// posts.lazy.tsx (遅延読み込みされます)
import { createLazyFileRoute } from '@tanstack/react-router'

export const Route = createLazyFileRoute('/posts')({
  component: PostsComponent,
  pendingComponent: PostsLoading,
  errorComponent: PostsError,
})

手動コード分割

const postsRoute = createRoute({
  getParentRoute: () => rootRoute,
  path: '/posts',
  loader: () => fetchPosts(),
}).lazy(() => import('./posts.lazy').then((d) => d.Route))

プリロード

// ルーターレベルのデフォルト
const router = createRouter({
  routeTree,
  defaultPreload: 'intent', // 'intent' | 'viewport' | 'render' | false
  defaultPreloadStaleTime: 30_000, // 30 秒
})

// ルートレベル
export const Route = createFileRoute('/posts/$postId')({
  // ローダーデータのステイル時間
  staleTime: 5_000,
  // プリロードされたデータが新鮮な状態を保つ時間
  preloadStaleTime: 30_000,
})

// Link レベル
<Link to="/posts" preload="intent" preloadDelay={100}>
  Posts
</Link>

型安全性

ルータータイプの登録

// 型推論のためのモジュール宣言
declare module '@tanstack/react-router' {
  interface Register {
    router: typeof router
  }
}

型安全なフック

すべてのフックはルートツリーに基づいて完全に型付けされます:

// useParams - ルートのパラメータに型付け
const { postId } = useParams({ from: '/posts/$postId' })

// useSearch - ルートの検索スキーマに型付け
const { page } = useSearch({ from: '/posts' })

// useLoaderData - ローダーの戻り値に型付け
const data = useLoaderData({ from: '/posts/$postId' })

// useRouteContext - ルートコンテキストに型付け
const { auth } = useRouteContext({ from: '/protected' })

ルートジェネリック

import { createFileRoute } from '@tanstack/react-router'

export const Route = createFileRoute('/posts/$postId')({
  // TypeScript が推論します:
  // params: { postId: string }
  // search: 検証された検索スキーマ型
  // loaderData: ローダーの戻り値の型
  // context: ルーターコンテキスト型
})

認証付きルート

// __root.tsx
export const Route = createRootRouteWithContext<{
  auth: AuthContext
}>()({
  component: RootComponent,
})

// _authenticated.tsx (認証用のパスレスレイアウト)
export const Route = createFileRoute('/_authenticated')({
  beforeLoad: ({ context, location }) => {
    if (!context.auth.isAuthenticated) {
      throw redirect({
        to: '/login',
        search: { redirect: location.href },
      })
    }
  },
})

// _authenticated/dashboard.tsx
export const Route = createFileRoute('/_authenticated/dashboard')({
  component: Dashboard, // 認証されている場合のみアクセス可能
})

スクロール復元

const router = createRouter({
  routeTree,
  // スクロール復元を有効化
  defaultScrollRestoration: true,
})

// またはルートごと
export const Route = createFileRoute('/posts')({
  // ナビゲーション時にトップにスクロール
  scrollRestoration: true,
})

// カスタムスクロール復元キー
<ScrollRestoration
  getKey={(location) => location.pathname}
/>

ルートマスキング

実際のルートとは異なる URL を表示します:

<Link
  to="/photos/$photoId"
  params={{ photoId: photo.id }}
  mask={{ to: '/photos', search: { photoId: photo.id } }}
>
  View Photo
</Link>

// またはプログラマティック
navigate({
  to: '/photos/$photoId',
  params: { photoId: photo.id },
  mask: { to: '/photos', search: { photoId: photo.id } },
})

見つからないハンドリング

// グローバル 404
const router = createRouter({
  routeTree,
  defaultNotFoundComponent: () => <div>Page not found</div>,
})

// ルートレベルの 404
export const Route = createFileRoute('/posts/$postId')({
  loader: async ({ params }) => {
    const post = await fetchPost(params.postId)
    if (!post) throw notFound()
    return post
  },
  notFoundComponent: () => <div>Post not found</div>,
})

ヘッド管理

export const Route = createFileRoute('/posts/$postId')({
  head: ({ loaderData }) => ({
    meta: [
      { title: loaderData.title },
      { name: 'description', content: loaderData.excerpt },
      { property: 'og:title', content: loaderData.title },
    ],
    links: [
      { rel: 'canonical', href: `https://example.com/posts/${loaderData.id}` },
    ],
  }),
})

TanStack Query との統合

import { queryOptions } from '@tanstack/react-query'

const postsQueryOptions = queryOptions({
  queryKey: ['posts'],
  queryFn: fetchPosts,
})

export const Route = createFileRoute('/posts')({
  loader: ({ context: { queryClient } }) => {
    // キャッシュにデータを確保、新鮮なら再フェッチしない
    return queryClient.ensureQueryData(postsQueryOptions)
  },
  component: PostsComponent,
})

function PostsComponent() {
  // リアクティブ更新のために同じクエリオプションを使用
  const { data: posts } = useSuspenseQuery(postsQueryOptions)
  return <PostsList posts={posts} />
}

ルーターフックリファレンス

フック目的
useRouter()ルーターインスタンスにアクセス
useRouterState()ルーター状態を購読
useParams()ルートパスパラメータを取得
useSearch()検証された検索パラメータを取得
useLoaderData()ルートローダーデータを取得
useRouteContext()ルートコンテキストを取得
useNavigate()ナビゲート関数を取得
useLocation()現在の場所を取得
useMatches()マッチしたすべてのルートを取得
useMatch()特定のルートマッチを取得
useBlocker()ナビゲーションをブロック
useLinkProps()カスタムコンポーネント用のリンク props を取得
useMatchRoute()ルートがマッチするかチェック

ベストプラクティス

  1. ファイルベースのルーティングを使用 - ほとんどのアプリケーションでは、より単純でルートツリーを自動生成します
  2. 検索パラメータを検証 - Zod またはカスタムバリデーターで型安全性を確保
  3. loaderDeps を使用 - 検索パラメータの変更に基づいてローダーが再実行される時期を制御
  4. コンテキストを活用 - 依存関係注入 (QueryClient、認証状態) に使用
  5. beforeLoad を使用 - コンポーネント内ではなく認証ガード用
  6. 重大コードと遅延コードを分離 - ローダーはメインファイルに、コンポーネントは .lazy.tsx
  7. Link で preload="intent" を使用 - 認識されるパフォーマンスの向上
  8. staleTime を使用 - ナビゲーション時の不要な再フェッチを防止
  9. ルータータイプを登録 - アプリ全体で完全な TypeScript 推論のため
  10. 404 状態に notFound() を使用 - 条件付きレンダリングの代わり
  11. 検索パラメータロジックをコロケート - それを所有するルートと一緒に
  12. パスレスレイアウト (_authenticated) を使用 - URL セグメントなしで認証/レイアウト共有ロジック用

よくある落とし穴

  • ルータータイプの登録 (declare module) を忘れる
  • ローダーが検索パラメータに依存する場合に loaderDeps を使用しない (古いデータになります)
  • コンポーネント内で認証チェックを行う代わりに beforeLoad で行わない (保護されたコンテンツのフラッシュ)
  • pendingComponent でロード状態を処理しない
  • ルートローダーの代わりに useEffect をデータフェッチに使用
  • navigate/Link を使用する代わりに検索パラメータを直接変更
  • アプリを RouterProvider でラップするのを忘れる
  • コードベースのルート定義で getParentRoute を忘れる

ライセンス: MIT(寛容ライセンスのため全文を引用しています) · 原本リポジトリ

詳細情報

作者
tanstack-skills
リポジトリ
tanstack-skills/tanstack-skills
ライセンス
MIT
最終更新
不明

Source: https://github.com/tanstack-skills/tanstack-skills / ライセンス: MIT

関連スキル

汎用ソフトウェア開発⭐ リポ 39,967

doubt-driven-development

重要な判断はすべて、本番環境への展開前に新しい視点から対抗的レビューを実施します。速度より正確性が重要な場合、不慣れなコードを扱う場合、本番環境・セキュリティに関わるロジック・取り消し不可の操作など影響度が高い場合、または後でバグを修正するよりも今検証する方が効率的な場合に活用してください。

by addyosmani
汎用ソフトウェア開発⭐ リポ 1,175

apprun-skills

TypeScriptを使用したAppRunアプリケーションのMVU設計に関する総合的なガイダンスが得られます。コンポーネントパターン、イベントハンドリング、状態管理(非同期ジェネレータを含む)、パラメータと保護機能を備えたルーティング・ナビゲーション、vistestを使用したテストに対応しています。AppRunコンポーネントの設計・レビュー、ルートの配線、状態フローの管理、AppRunテストの作成時に活用してください。

by yysun
OpenAIソフトウェア開発⭐ リポ 797

desloppify

コードベースのヘルスチェックと技術負債の追跡ツールです。コード品質、技術負債、デッドコード、大規模ファイル、ゴッドクラス、重複関数、コードスメル、命名規則の問題、インポートサイクル、結合度の問題についてユーザーが質問した場合に使用してください。また、ヘルススコアの確認、次の改善項目の提案、クリーンアップ計画の作成をリクエストされた際にも対応します。29言語に対応しています。

by Git-on-my-level
汎用ソフトウェア開発⭐ リポ 39,967

debugging-and-error-recovery

テストが失敗したり、ビルドが壊れたり、動作が期待と異なったり、予期しないエラーが発生したりした場合に、体系的な根本原因デバッグをガイドします。推測ではなく、根本原因を見つけて修正するための体系的なアプローチが必要な場合に使用してください。

by addyosmani
汎用ソフトウェア開発⭐ リポ 39,967

test-driven-development

テスト駆動開発により実装を進めます。ロジックの実装、バグの修正、動作の変更など、あらゆる場面で活用できます。コードが正常に動作することを証明する必要がある場合、バグ報告を受けた場合、既存機能を修正する予定がある場合に使用してください。

by addyosmani
汎用ソフトウェア開発⭐ リポ 39,967

incremental-implementation

変更を段階的に実施します。複数のファイルに影響する機能や変更を実装する場合に使用してください。大量のコードを一度に書こうとしている場合や、タスクが一度では完結できないほど大きい場合に活用します。

by addyosmani
本サイトは GitHub 上で公開されているオープンソースの SKILL.md ファイルをクロール・インデックス化したものです。 各スキルの著作権は原作者に帰属します。掲載に問題がある場合は info@alsel.co.jp または /takedown フォームよりご連絡ください。
原作者: tanstack-skills · tanstack-skills/tanstack-skills · ライセンス: MIT