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 |
$.tsx | Splat/キャッチオール | /* |
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() | ルートがマッチするかチェック |
ベストプラクティス
- ファイルベースのルーティングを使用 - ほとんどのアプリケーションでは、より単純でルートツリーを自動生成します
- 検索パラメータを検証 - Zod またはカスタムバリデーターで型安全性を確保
loaderDepsを使用 - 検索パラメータの変更に基づいてローダーが再実行される時期を制御- コンテキストを活用 - 依存関係注入 (QueryClient、認証状態) に使用
beforeLoadを使用 - コンポーネント内ではなく認証ガード用- 重大コードと遅延コードを分離 - ローダーはメインファイルに、コンポーネントは
.lazy.tsxに - Link で
preload="intent"を使用 - 認識されるパフォーマンスの向上 staleTimeを使用 - ナビゲーション時の不要な再フェッチを防止- ルータータイプを登録 - アプリ全体で完全な TypeScript 推論のため
- 404 状態に
notFound()を使用 - 条件付きレンダリングの代わり - 検索パラメータロジックをコロケート - それを所有するルートと一緒に
- パスレスレイアウト (
_authenticated) を使用 - URL セグメントなしで認証/レイアウト共有ロジック用
よくある落とし穴
- ルータータイプの登録 (
declare module) を忘れる - ローダーが検索パラメータに依存する場合に
loaderDepsを使用しない (古いデータになります) - コンポーネント内で認証チェックを行う代わりに
beforeLoadで行わない (保護されたコンテンツのフラッシュ) pendingComponentでロード状態を処理しない- ルートローダーの代わりに
useEffectをデータフェッチに使用 - navigate/Link を使用する代わりに検索パラメータを直接変更
- アプリを
RouterProviderでラップするのを忘れる - コードベースのルート定義で
getParentRouteを忘れる
ライセンス: MIT(寛容ライセンスのため全文を引用しています) · 原本リポジトリ
詳細情報
- 作者
- tanstack-skills
- ライセンス
- MIT
- 最終更新
- 不明
Source: https://github.com/tanstack-skills/tanstack-skills / ライセンス: MIT
関連スキル
doubt-driven-development
重要な判断はすべて、本番環境への展開前に新しい視点から対抗的レビューを実施します。速度より正確性が重要な場合、不慣れなコードを扱う場合、本番環境・セキュリティに関わるロジック・取り消し不可の操作など影響度が高い場合、または後でバグを修正するよりも今検証する方が効率的な場合に活用してください。
apprun-skills
TypeScriptを使用したAppRunアプリケーションのMVU設計に関する総合的なガイダンスが得られます。コンポーネントパターン、イベントハンドリング、状態管理(非同期ジェネレータを含む)、パラメータと保護機能を備えたルーティング・ナビゲーション、vistestを使用したテストに対応しています。AppRunコンポーネントの設計・レビュー、ルートの配線、状態フローの管理、AppRunテストの作成時に活用してください。
desloppify
コードベースのヘルスチェックと技術負債の追跡ツールです。コード品質、技術負債、デッドコード、大規模ファイル、ゴッドクラス、重複関数、コードスメル、命名規則の問題、インポートサイクル、結合度の問題についてユーザーが質問した場合に使用してください。また、ヘルススコアの確認、次の改善項目の提案、クリーンアップ計画の作成をリクエストされた際にも対応します。29言語に対応しています。
debugging-and-error-recovery
テストが失敗したり、ビルドが壊れたり、動作が期待と異なったり、予期しないエラーが発生したりした場合に、体系的な根本原因デバッグをガイドします。推測ではなく、根本原因を見つけて修正するための体系的なアプローチが必要な場合に使用してください。
test-driven-development
テスト駆動開発により実装を進めます。ロジックの実装、バグの修正、動作の変更など、あらゆる場面で活用できます。コードが正常に動作することを証明する必要がある場合、バグ報告を受けた場合、既存機能を修正する予定がある場合に使用してください。
incremental-implementation
変更を段階的に実施します。複数のファイルに影響する機能や変更を実装する場合に使用してください。大量のコードを一度に書こうとしている場合や、タスクが一度では完結できないほど大きい場合に活用します。