TanStack Router 2025 - 型安全ルーティングの新スタンダード

2026.04.10

公式ドキュメント

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>
 );
}
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 RouterReact Router v7Next.js App Router
型安全極めて高い中 (v7 で改善)
ファイルベース対応 (任意)対応必須
検索パラメータ検証ビルトイン手動手動
データローダーありありServer Components
フレームワークSPA/SSRSPA/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: pendingComponentloader の組み合わせで 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 は第一候補です。

参考リソース

← 一覧に戻る
PR
PR
PR
PR