この記事の要点
• TanStack RouterはTypeScriptの型を完全に統合した最も型安全なルーティングライブラリ
• ファイルベースルーティング、検索パラメータの型検証、データローダーを型レベルで統合
• React Router/Next.jsとは異なるアプローチで型安全性を追求
TanStack Router は React 向けの型安全なルーティングライブラリです。ファイルベースルーティング、検索パラメータの型検証、データローダー、コード分割をすべてを型レベルで統合しており、React Router や Next.js App Router とは異なるアプローチで「最も型安全な router」を標榜しています。
TanStack Routerの概要
背景
SPA のルーティングライブラリは長らく useParams() や useSearchParams() が any / string を返すだけでした。TanStack Router は Router 定義から TypeScript の型を自動生成し、ルートパス、パラメータ、検索パラメータ、ローダ戻り値までを完全に型付けします。
アーキテクチャ
flowchart TB
Routes["routes/ (file-based)"] --> Generator["@tanstack/router-plugin"]
Generator --> RouteTree["routeTree.gen.ts"]
RouteTree --> Router["createRouter()"]
Router --> RouterProvider["<RouterProvider />"]
RouterProvider --> Components["Route components"]
Components --> Loader["beforeLoad / loader"]
Loader --> Cache["Router cache"]
主要機能詳細
1. ファイルベースルーティング
src/routes/
__root.tsx
index.tsx -> /
about.tsx -> /about
posts/
index.tsx -> /posts
$postId.tsx -> /posts/:postId
$postId.edit.tsx -> /posts/:postId/edit
_authed/ -> Layout route
dashboard.tsx -> /dashboard
// src/routes/__root.tsx
import { Outlet, createRootRoute } from '@tanstack/react-router';
export const Route = createRootRoute({
component: () => (
<div>
<nav>…</nav>
<Outlet />
</div>
),
});
// src/routes/posts/$postId.tsx
import { createFileRoute } from '@tanstack/react-router';
export const Route = createFileRoute('/posts/$postId')({
loader: async ({ params }) => {
const res = await fetch(`/api/posts/${params.postId}`);
if (!res.ok) throw new Error('not found');
return res.json() as Promise<{ id: string; title: string; body: string }>;
},
component: PostPage,
});
function PostPage() {
const post = Route.useLoaderData();
const { postId } = Route.useParams();
return (
<article>
<h1>{post.title}</h1>
<p>{post.body}</p>
</article>
);
}
2. 検索パラメータのスキーマ検証
import { createFileRoute } from '@tanstack/react-router';
import { z } from 'zod';
const searchSchema = z.object({
page: z.number().int().min(1).catch(1),
q: z.string().optional(),
sort: z.enum(['new', 'old']).catch('new'),
});
export const Route = createFileRoute('/posts/')({
validateSearch: searchSchema,
loaderDeps: ({ search }) => ({ page: search.page, q: search.q, sort: search.sort }),
loader: async ({ deps }) => {
const params = new URLSearchParams({
page: String(deps.page),
...(deps.q ? { q: deps.q } : {}),
sort: deps.sort,
});
const res = await fetch(`/api/posts?${params}`);
return res.json();
},
component: PostList,
});
function PostList() {
const { page, q, sort } = Route.useSearch();
const navigate = Route.useNavigate();
return (
<div>
<input
value={q ?? ''}
onChange={e => navigate({
search: prev => ({ ...prev, q: e.target.value, page: 1 }),
})}
/>
<button onClick={() => navigate({ search: prev => ({ ...prev, page: prev.page + 1 }) })}>
Next
</button>
</div>
);
}
3. 型安全な Link
import { Link } from '@tanstack/react-router';
<Link
to="/posts/$postId"
params={{ postId: '42' }}
search={{ ref: 'sidebar' }}
>
投稿を見る
</Link>
存在しないパス、欠落したパラメータ、型不一致は TypeScript が検出します。
4. beforeLoad による認可
// src/routes/_authed.tsx
import { createFileRoute, redirect } from '@tanstack/react-router';
export const Route = createFileRoute('/_authed')({
beforeLoad: ({ context, location }) => {
if (!context.auth.isAuthenticated) {
throw redirect({
to: '/login',
search: { redirect: location.href },
});
}
},
});
5. ルートコンテキスト
// src/router.tsx
import { createRouter } from '@tanstack/react-router';
import { routeTree } from './routeTree.gen';
import { QueryClient } from '@tanstack/react-query';
const queryClient = new QueryClient();
export const router = createRouter({
routeTree,
context: {
queryClient,
auth: undefined!, // will be filled by <RouterProvider context={…} />
},
defaultPreload: 'intent',
});
declare module '@tanstack/react-router' {
interface Register { router: typeof router }
}
// src/main.tsx
import { RouterProvider } from '@tanstack/react-router';
import { useAuth } from './auth';
import { router } from './router';
function App() {
const auth = useAuth();
return <RouterProvider router={router} context={{ auth }} />;
}
実践サンプル
React Query との統合
// src/routes/users/$userId.tsx
import { createFileRoute } from '@tanstack/react-router';
import { queryOptions, useSuspenseQuery } from '@tanstack/react-query';
const userQuery = (userId: string) =>
queryOptions({
queryKey: ['users', userId],
queryFn: async () => {
const res = await fetch(`/api/users/${userId}`);
return res.json();
},
});
export const Route = createFileRoute('/users/$userId')({
loader: ({ context, params }) =>
context.queryClient.ensureQueryData(userQuery(params.userId)),
component: UserPage,
});
function UserPage() {
const { userId } = Route.useParams();
const { data: user } = useSuspenseQuery(userQuery(userId));
return <h1>{user.name}</h1>;
}
コード分割
import { createFileRoute, lazyRouteComponent } from '@tanstack/react-router';
export const Route = createFileRoute('/settings')({
component: lazyRouteComponent(() => import('./-components/Settings')),
});
エラーバウンダリ
export const Route = createFileRoute('/posts/$postId')({
errorComponent: ({ error, reset }) => (
<div>
<p>Error: {error.message}</p>
<button onClick={reset}>Retry</button>
</div>
),
pendingComponent: () => <p>Loading…</p>,
component: PostPage,
});
Vite 設定
// vite.config.ts
import { defineConfig } from 'vite';
import react from '@vitejs/plugin-react';
import { TanStackRouterVite } from '@tanstack/router-plugin/vite';
export default defineConfig({
plugins: [
TanStackRouterVite({
routesDirectory: './src/routes',
generatedRouteTree: './src/routeTree.gen.ts',
}),
react(),
],
});
比較表
ルーティングライブラリ比較
| 項目 | TanStack Router | React Router v7 | Next.js App Router |
|---|---|---|---|
| 型安全 | 極めて高い | 中 (v7 で改善) | 中 |
| ファイルベース | 対応 (任意) | 対応 | 必須 |
| 検索パラメータ検証 | ビルトイン | 手動 | 手動 |
| データローダー | あり | あり | Server Components |
| フレームワーク | SPA/SSR | SPA/SSR | フルスタック |
| コード分割 | ネイティブ | ネイティブ | ネイティブ |
ポイント: TanStack Routerはルート定義からTypeScriptの型を自動生成するため、パラメータの型ミスをコンパイル時に検出できます。
ベストプラクティス
1. 検索パラメータはスキーマで守る
Zod/Valibot スキーマで validateSearch を定義し、不正値を .catch でフォールバック。
2. Loader はコンテキスト経由でデータソースを受け取る
context.queryClient などを経由させ、テスト時に差し替え可能にします。
3. defaultPreload: 'intent' でリンクホバー時にプリフェッチ
ユーザ体感速度が大きく向上します。
4. ルートツリー生成ファイルを Git 管理する
routeTree.gen.ts をコミットしておくと CI での初回ビルドが安定します。
5. 深い階層は Layout Route で共通化
_authed / _admin のような underscore prefix ディレクトリで共通レイアウトを表現します。
実践メモ: defaultPreload: 'intent' を設定すると、リンクにホバーした時点でデータのプリフェッチが開始され、体感速度が大幅に向上します。
注意点
- TanStack Router はもともと SPA 向けに設計されています。SSR / SSG を使いたい場合は TanStack Start を併用します。
- React Router からの移行は API 概念が大きく異なるため、段階的に行うのが安全です。
- ルートコンテキストの
undefined!キャストは起動時に必ずRouterProviderで値を渡すことが前提です。 - 検索パラメータの正規化 (順序・空値) により URL が書き換えられるため、外部からの URL 生成では
validateSearchの結果を再利用します。 - コード生成を使うため、IDE のリスタートが必要になる場面があります。
注意: TanStack RouterはReact専用であり、Vue/Svelte等の他フレームワークには対応していません。また、SSR機能はTanStack Startと組み合わせる必要があります。
導入手順
pnpm add @tanstack/react-router
pnpm add -D @tanstack/router-plugin
// src/main.tsx
import React from 'react';
import ReactDOM from 'react-dom/client';
import { RouterProvider, createRouter } from '@tanstack/react-router';
import { routeTree } from './routeTree.gen';
const router = createRouter({ routeTree });
declare module '@tanstack/react-router' {
interface Register { router: typeof router }
}
ReactDOM.createRoot(document.getElementById('root')!).render(
<React.StrictMode>
<RouterProvider router={router} />
</React.StrictMode>,
);
パフォーマンス
プリロード戦略
createRouter({
routeTree,
defaultPreload: 'intent', // 'intent' | 'viewport' | 'render' | false
defaultPreloadStaleTime: 30_000,
});
intent: ホバー / フォーカス時viewport: リンクがビューポートに入ったときrender: レンダリング時
バンドルサイズ
Router 本体は比較的小さく、さらに lazyRouteComponent で各ルートを分割することで初回ロードを軽量化できます。
FAQ
Q: Next.js と併用できますか? A: Next.js の App Router はフレームワーク内蔵の router なので併用は基本しません。SPA で使うか TanStack Start を選びます。
Q: React Router から移行できますか? A: API が大きく異なるため書き換えが必要ですが、型安全性の恩恵は大きいです。
Q: Suspense ベースのローディングに対応していますか?
A: pendingComponent と loader の組み合わせで Suspense 的な挙動を実現できます。
Q: 非 React で使えますか? A: React 版が中心ですが、コア API は framework-agnostic で Solid / Vue 対応が進行中です。
Q: SEO はどうなる? A: SPA モードではクライアントレンダリングのため、SEO を重視する場合は TanStack Start などの SSR 構成を使います。
さらなる応用
認証と redirect パラメータ
// src/routes/login.tsx
import { createFileRoute, useNavigate } from '@tanstack/react-router';
import { z } from 'zod';
export const Route = createFileRoute('/login')({
validateSearch: z.object({
redirect: z.string().optional().catch(''),
}),
component: LoginPage,
});
function LoginPage() {
const { redirect } = Route.useSearch();
const navigate = useNavigate();
const onSubmit = async () => {
// login…
navigate({ to: redirect || '/dashboard' });
};
return <form onSubmit={onSubmit}>…</form>;
}
ネストされたローダー
// src/routes/orgs/$orgId.tsx
export const Route = createFileRoute('/orgs/$orgId')({
loader: async ({ params, context }) =>
context.queryClient.ensureQueryData({
queryKey: ['org', params.orgId],
queryFn: () => fetch(`/api/orgs/${params.orgId}`).then(r => r.json()),
}),
});
// src/routes/orgs/$orgId/members.tsx
export const Route = createFileRoute('/orgs/$orgId/members')({
loader: async ({ params, context }) =>
context.queryClient.ensureQueryData({
queryKey: ['org', params.orgId, 'members'],
queryFn: () => fetch(`/api/orgs/${params.orgId}/members`).then(r => r.json()),
}),
});
親子のローダーは独立してキャッシュされ、親データの再取得なしに子のみを更新できます。
devtools
import { TanStackRouterDevtools } from '@tanstack/react-router-devtools';
export const Route = createRootRoute({
component: () => (
<>
<Outlet />
{import.meta.env.DEV && <TanStackRouterDevtools />}
</>
),
});
開発時にルートツリー、マッチング、ローダ状態を視覚的に確認できます。
まとめ
TanStack Router は「型安全」という軸で他の router を圧倒する設計を持ち、特に検索パラメータと loader の型統合は他に代えがたい価値があります。React Query と組み合わせると、データ取得、キャッシュ、ルーティング、型安全性がひとつの一貫した API として扱えます。SPA 構成で型の恩恵を最大化したいなら、TanStack Router は第一候補です。