tanstack-router について
TanStack Router向けの開発スキルです。React開発者を対象に、ファイルベースのルート設定、検索パラメーターの検証、loader、アクセスガード、型付きナビゲーションを解説します。
主な機能
- 01型安全なルートとナビゲーション
- 02ファイルベースのルート規約
- 03検索パラメーターの検証
- 04loaderによるデータ取得
- 05beforeLoadによるアクセス確認
活用シーン
- 01Reactアプリへの型付きルート追加
- 02クエリパラメーターの検証
- 03データ読み込み前のルート保護
インストール
コマンドをコピーしてターミナルで実行します。
$
npx skills add https:/ / github.com/ mindrally/ skills --skill tanstack-routerインストール後、Claude Code で/tanstack-routerを入力して読み込まれたか確認します。
詳細
TanStack Router
TanStack Router は、ルート、ローダー、パラメーター、検索パラメーターを型のない文字列ではなく、第一級の型付きデータとして扱う、React 用の完全に型安全なルーターです。
新しいルートを追加するワークフロー
- ルートファイルを作成する — ファイルベースルーティングの規約(下記参照)に従って、
src/routes/の下にファイルを追加します。 - ルートを定義する —
createFileRoute('/path')({...})からRouteをエクスポートし、componentを指定します。必要に応じて、loader、validateSearch、beforeLoad、errorComponent、pendingComponentも指定します。 - 検索パラメーターを検証する — ルートでクエリパラメーターを読み取る場合は、Zod スキーマを定義して
validateSearchとして渡します。 - データを読み込む — データはコンポーネントの
useEffectではなく、loaderで取得します。キャッシュが必要な場合は、TanStack Query とqueryClient.ensureQueryDataを連携させます。 - アクセスを制御する — 事前条件を満たさない場合に
throw redirect({ to: '/login' })するbeforeLoadチェック(認証など)を追加します。 - ルートへのリンクを作成する —
<Link to="/path" params={...} search={...}>で遷移し、すべての呼び出し箇所でコンパイラーにパラメーターと検索パラメーターを検証させます。 - ルートツリーを再生成する — アプリの実行またはビルドの前に、
routeTree.gen.tsが再生成されていることを確認します(Vite プラグインの開発サーバー/ビルドでは自動的に再生成されます)。
基本原則
- TanStack Router は 100% 型安全です。手動で型変換する代わりに、パラメーター、検索パラメーター、ローダーデータには TypeScript のジェネリクスを活用します。
- 手動でルートツリーを構築するよりも、拡張性の高いファイルベースルーティングを
@tanstack/router-vite-plugin(または@tanstack/router-plugin/vite)とともに使用します。 - ルートは必ず、
createFileRoute(リーフ/ネストルート)またはcreateRootRoute/createRootRouteWithContext(ルート)で定義します。 - ルートデータの読み込みは、コンポーネントの
useEffectではなく、loader関数で行います。これにより、事前読み込み、並列読み込み、保留中/エラー状態が利用できます。 - 検索パラメーターは第一級の状態です。読み取るたびに型付けと検証が行われるよう、必ず Zod(または別の標準スキーマバリデーター)でスキーマを定義します。
ファイルベースのルート規約
src/routes/
__root.tsx ← Root layout
index.tsx ← / route
posts/
index.tsx ← /posts
$postId.tsx ← /posts/:postId (dynamic segment)
_layout.tsx ← Layout route (no path segment)
_auth/ ← Pathless auth layout group
dashboard.tsx
- セグメントの先頭にアンダースコアを付けると(
_layout、_auth)、グループ化や共通 UI 専用のパスレスレイアウトルートが作成されます。 $プレフィックス($postId)は、Route.useParams()に対応する動的パスセグメントを示します。- フォルダー内の
index.tsxは、追加のセグメントなしで、そのフォルダー自体のパスに一致します。
ルート定義
export const Route = createFileRoute('/posts/$postId')({
loader: async ({ params }) => fetchPost(params.postId),
component: PostComponent,
errorComponent: ({ error }) => <ErrorBanner message={error.message} />,
pendingComponent: () => <PostSkeleton />,
})
function PostComponent() {
const post = Route.useLoaderData() // type-safe
const { postId } = Route.useParams() // type-safe
return <div>{post.title}</div>
}
errorComponentはローダーがエラーをスローしたときにレンダリングされます。pendingComponentはローダーの処理中にレンダリングされます(defaultPendingMsの設定によります)。- ローダーの戻り値とスローされたエラーは、どちらも完全に型付けされ、それぞれ
useLoaderData()とerrorComponentに渡されます。
型安全な検索パラメーター
- 検索パラメーターは必ず Zod と
validateSearchで定義します。 Route.useSearch()でアクセスします。型安全性と検証を回避してしまうため、window.location.searchやURLSearchParamsを直接読み取ってはいけません。
const searchSchema = z.object({
page: z.number().int().min(1).default(1),
q: z.string().optional(),
})
export const Route = createFileRoute('/search')({
validateSearch: searchSchema,
component: SearchPage,
})
function SearchPage() {
const { page, q } = Route.useSearch()
const navigate = Route.useNavigate()
return (
<button onClick={() => navigate({ search: (prev) => ({ ...prev, page: page + 1 }) })}>
Next page
</button>
)
}
ナビゲーション
- 内部ナビゲーションには
<Link>を使用します。ページ全体が再読み込みされ、クライアント側ルーティングの状態が失われるため、生の<a href>は使用しないでください。 - 型付けされた
paramsとsearchを必ず渡します。TypeScript コンパイラーが、ビルド時にルートパラメーターの不足や型の不一致を検出します。
<Link to="/posts/$postId" params={{ postId: '123' }}>View Post</Link>
<Link to="/search" search={{ page: 1, q: 'react' }}>Search</Link>
ローダーと TanStack Query の連携
- ローダーが TanStack Query を通じてデータを事前取得・キャッシュできるように、
QueryClientをルーターコンテキストに配置します。取得ロジックの重複を避けられます。 - ローダーでは
fetchQueryではなくensureQueryDataを使用します。すでにキャッシュされているクエリを再取得せずに再利用できます。
export const Route = createFileRoute('/posts')({
loader: ({ context: { queryClient } }) =>
queryClient.ensureQueryData(postsQueryOptions()),
component: PostsPage,
})
function PostsPage() {
const { data: posts } = useSuspenseQuery(postsQueryOptions())
return <PostList posts={posts} />
}
依存性注入のためのルーターコンテキスト
// __root.tsx
interface RouterContext {
queryClient: QueryClient
auth: AuthState
}
export const Route = createRootRouteWithContext<RouterContext>()({
component: RootLayout,
})
// main.tsx
const router = createRouter({ routeTree, context: { queryClient, auth } })
- ルーターコンテキストは
context引数を介して、すべてのルートのloaderとbeforeLoadに渡されます。これにより、プロパティーのバケツリレーやグローバルシングルトンを使わずに、各ルートから共有依存関係へ型付きでアクセスできます。
認証ガード
export const Route = createFileRoute('/_auth/dashboard')({
beforeLoad: ({ context }) => {
if (!context.auth.isAuthenticated) throw redirect({ to: '/login' })
},
component: Dashboard,
})
beforeLoadはローダーの実行前、コンポーネントのレンダリング前に実行されるため、認証チェック、機能フラグによる制御、リダイレクトに適しています。- コンポーネント内で命令的に画面遷移するより、
throw redirect(...)を優先してください。SSR、プリロード、クライアント側の画面遷移のいずれでも動作します。
パフォーマンス
- ルーターに
defaultPreload: 'intent'を設定すると、リンクにホバーまたはフォーカスしたときにルートのデータがプリフェッチされ、瞬時に画面遷移するように感じられます。 - 大規模なアプリでは、ルートコンポーネントのコード分割に
React.lazy(またはルーター組み込みのコード分割機能である.lazy()ルートファイル)を使用してください。 @tanstack/router-devtoolsをインストールし、開発時に<TanStackRouterDevtools />をレンダリングすると、ルートの一致、保留中の状態、キャッシュを確認できます。
よくある間違い
- ルートコンポーネント内で
loaderではなくuseEffectを使ってデータを取得すると、プリロードができなくなり、レンダリング後に取得するウォーターフォールが発生します。 Route.useSearch()ではなく生のwindow.location.searchを読み取ると、検索パラメーターのスキーマと型安全性を迂回してしまいます。beforeLoadではなくコンポーネント内だけで認証チェック(useEffectによるリダイレクト)を行うと、リダイレクト前に保護対象のコンテンツが一瞬表示されます。- 新しいルートパラメーターや検索フィールドを
<Link>の呼び出し箇所に追加し忘れないでください。手動でテストするのではなく、コンパイラーに検出させましょう。
コメント
Markdown 対応