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

2026.04.10

公式ドキュメント

この記事の要点

• 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>
  );
}
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 は第一候補です。

参考リソース

この技術を体系的に学びたいですか?

未来学では東証プライム上場企業のITエンジニアが24時間サポート。月額24,800円から、退会金0円のオンラインIT塾です。

メールで無料相談する
← 一覧に戻る