※本記事には広告(楽天アフィリエイト)リンクが含まれる場合があります。

「useParams() の返り値が全部 string | undefined で型が効かない」「検索パラメータをクエリ文字列で手書きしていると型が壊れる」——React Router を使っていて、こうした型の不満を感じたことはありませんか?

TanStack Router はこれらの問題をゼロから解決するために設計された、TypeScript-first のルーティングライブラリです。ルートパラメータ・検索パラメータ・ローダーのレスポンスまで完全に型が通り、navigate() に存在しないパスを渡すとコンパイルエラーになります。Remix や React Router v7 とも異なる独自の設計思想を持ち、2026年の個人開発・チーム開発の両方で採用が急増しています。

この記事では、TanStack Router をゼロから学んで個人開発に組み込むまでの知識を、実際のコードとともに解説します。

広告・楽天アフィリエイト

プログラミング技術書

実践寄りの技術書・参考書を探す。

楽天ポイント還元

技術書を楽天で見る →

TanStack Routerとは?React Routerとの違い

TanStack Router は、TanStack(TanStack Query・Table・Form などを手がけるチーム)が開発するルーティングライブラリです。クライアントサイド・SSRの両方をサポートし、2026年現在の最新バージョンは v1.x です。

React Router(v7)との主な違いは次のとおりです。

比較項目 TanStack Router React Router v7
ルートパラメータの型 ◎ 完全型推論 △ string のみ
検索パラメータ ◎ スキーマで型付け △ 文字列のみ
ローダーの型 ◎ ローダー戻り値を自動推論 ○ 手動で型付けが必要
ファイルベースルーティング ◎ コード生成で完全型安全 ○ v7から対応
SSR/サーバー側ルーティング △ TanStack Start経由 ◎ Remix由来で成熟
エコシステム ○ TanStack Queryと相性◎ ◎ 大きなエコシステム

「型安全なナビゲーション」「検索パラメータのスキーマ管理」が必要なら TanStack Router が光ります。一方、SSR を中心に考えるなら React Router v7(Remix)の方が成熟しています。

インストールとセットアップ

Vite + React の環境を前提に進めます。

npm create vite@latest my-app -- --template react-ts
cd my-app
npm install @tanstack/react-router
npm install -D @tanstack/router-plugin @tanstack/router-devtools

ファイルベースルーティングを使う場合は 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(),
    react(),
  ],
});

プラグインを有効にすると、src/routes/ ディレクトリのファイル構成を元に src/routeTree.gen.ts が自動生成されます。このファイルに完全な型情報が含まれます。

ファイルベースルーティング:ディレクトリ構成

TanStack Router のファイルベースルーティングは Next.js のそれに近いですが、命名規則が少し異なります。

src/routes/
├── __root.tsx          # ルートレイアウト(常にマウントされる)
├── index.tsx           # / (トップページ)
├── about.tsx           # /about
├── posts/
│   ├── index.tsx       # /posts
│   └── $postId.tsx     # /posts/:postId  (動的セグメント)
├── _auth/              # _ から始まるフォルダはレイアウトルート(URLに影響しない)
│   ├── route.tsx       # /の下に認証レイアウト
│   ├── dashboard.tsx   # /dashboard
│   └── settings.tsx    # /settings
└── (marketing)/        # () はパスレスグループ
    └── landing.tsx     # /landing

命名ルールのポイントです。

  • $param — 動的セグメント(:param 相当)
  • _layout/ — URL に含まれないレイアウトフォルダ
  • (group)/ — グルーピング専用(URL に影響しない)
  • __root.tsx — すべてのルートの親となるルートコンポーネント

ルートコンポーネントの書き方

各ルートファイルでは createFileRoute を使って定義します。

// src/routes/index.tsx
import { createFileRoute } from "@tanstack/react-router";

export const Route = createFileRoute("/")({
  component: IndexPage,
});

function IndexPage() {
  return (
    <div>
      <h1>ホーム</h1>
    </div>
  );
}

ルートレイアウトファイルには Outlet を置いて子ルートを描画します。

// src/routes/__root.tsx
import { createRootRoute, Link, Outlet } from "@tanstack/react-router";
import { TanStackRouterDevtools } from "@tanstack/router-devtools";

export const Route = createRootRoute({
  component: RootLayout,
});

function RootLayout() {
  return (
    <>
      <nav>
        <Link to="/">ホーム</Link>
        <Link to="/posts">記事一覧</Link>
      </nav>
      <Outlet />
      <TanStackRouterDevtools />
    </>
  );
}

型安全なルートパラメータ

TanStack Router の最大の強みが、動的セグメントの型推論です。

// src/routes/posts/$postId.tsx
import { createFileRoute } from "@tanstack/react-router";

export const Route = createFileRoute("/posts/$postId")({
  component: PostPage,
});

function PostPage() {
  // postId は string 型として確定(undefined がない)
  const { postId } = Route.useParams();

  return <h1>記事 ID: {postId}</h1>;
}

useParams() の返り値は { postId: string } と推論されます。string | undefined ではないので、null チェックが不要です。

さらに params オプションでパースと変換もできます。

export const Route = createFileRoute("/posts/$postId")({
  params: {
    parse: (params) => ({
      postId: Number(params.postId), // string → number に変換
    }),
    stringify: (params) => ({
      postId: String(params.postId),
    }),
  },
  component: PostPage,
});

function PostPage() {
  const { postId } = Route.useParams();
  // postId は number 型
}

型安全な検索パラメータ(Search Params)

TanStack Router が特に優れているのが、URLの検索パラメータ(?page=1&sort=date など)の型管理です。Zod や Valibot で定義したスキーマをルートに渡すだけです。

// src/routes/posts/index.tsx
import { createFileRoute } from "@tanstack/react-router";
import { z } from "zod";

const searchSchema = z.object({
  page: z.number().int().min(1).catch(1),
  sort: z.enum(["date", "title", "views"]).catch("date"),
  q: z.string().optional(),
});

export const Route = createFileRoute("/posts/")({
  validateSearch: searchSchema,
  component: PostListPage,
});

function PostListPage() {
  // page は number, sort は "date" | "title" | "views", q は string | undefined
  const { page, sort, q } = Route.useSearch();

  return (
    <div>
      <p>ページ: {page} / ソート: {sort}</p>
    </div>
  );
}

validateSearch に渡したスキーマによって、URLが壊れていてもデフォルト値にフォールバックされます(.catch() の挙動)。従来のように「クエリ文字列を自分でパース→型アサーション」という危うい実装が不要になります。

ローダー:データ取得の型推論

ルートに loader を定義すると、コンポーネントが表示される前にデータを取得できます。ローダーの戻り値は自動的にコンポーネントの型に伝わります。

// src/routes/posts/$postId.tsx
import { createFileRoute } from "@tanstack/react-router";

async function fetchPost(postId: string) {
  const res = await fetch(`/api/posts/${postId}`);
  if (!res.ok) throw new Error("記事が見つかりません");
  return res.json() as Promise<{ id: string; title: string; body: string }>;
}

export const Route = createFileRoute("/posts/$postId")({
  loader: ({ params }) => fetchPost(params.postId),
  component: PostPage,
});

function PostPage() {
  // data は { id: string; title: string; body: string } と推論される
  const post = Route.useLoaderData();

  return (
    <article>
      <h1>{post.title}</h1>
      <p>{post.body}</p>
    </article>
  );
}

ローダーの実行はルートのマッチング時に行われるため、コンポーネントが描画される時点でデータは必ず存在します。TanStack Query との統合も公式でサポートされており、loader でキャッシュをプリフェッチする使い方が推奨パターンです。

TanStack Queryとの組み合わせ

TanStack Router は同じ TanStack ファミリーの TanStack Query(旧React Query)との組み合わせが最強です。

// src/routes/posts/$postId.tsx
import { createFileRoute } from "@tanstack/react-router";
import { queryOptions, useSuspenseQuery } from "@tanstack/react-query";

const postQueryOptions = (postId: string) =>
  queryOptions({
    queryKey: ["posts", postId],
    queryFn: () => fetch(`/api/posts/${postId}`).then((r) => r.json()),
  });

export const Route = createFileRoute("/posts/$postId")({
  loader: ({ params, context: { queryClient } }) =>
    queryClient.ensureQueryData(postQueryOptions(params.postId)),
  component: PostPage,
});

function PostPage() {
  const { postId } = Route.useParams();
  // ローダーでプリフェッチ済みなのでサスペンスしない
  const { data: post } = useSuspenseQuery(postQueryOptions(postId));

  return <h1>{post.title}</h1>;
}

この組み合わせパターンでは、ルーティング遷移時にローダーでキャッシュを先読みし、コンポーネント側では useSuspenseQuery でデータを消費します。キャッシュの恩恵でバック/フォワード操作も爆速になります。

ネストルートとレイアウト

複数のページで共通レイアウト(サイドバー・ヘッダーなど)を使う場合はネストルートを使います。

src/routes/
├── _auth/
│   ├── route.tsx        # 認証後UIの共通レイアウト
│   ├── dashboard.tsx    # /dashboard
│   └── settings.tsx     # /settings
// src/routes/_auth/route.tsx(共通レイアウト)
import { createFileRoute, Outlet, redirect } from "@tanstack/react-router";

export const Route = createFileRoute("/_auth")({
  beforeLoad: ({ context }) => {
    if (!context.auth.isAuthenticated) {
      throw redirect({ to: "/login" });
    }
  },
  component: AuthLayout,
});

function AuthLayout() {
  return (
    <div className="layout">
      <aside>
        <nav>
          <Link to="/dashboard">ダッシュボード</Link>
          <Link to="/settings">設定</Link>
        </nav>
      </aside>
      <main>
        <Outlet />
      </main>
    </div>
  );
}

beforeLoad はルートに入る前に実行されるフックで、認証チェックやリダイレクトに使います。

認証ガードの実装

TanStack Router での認証実装のベストプラクティスは、RouterContext(ルーターコンテキスト)に認証情報を渡す方法です。

// src/main.tsx
import { RouterProvider, createRouter } from "@tanstack/react-router";
import { routeTree } from "./routeTree.gen";
import { useAuth } from "./auth";

const router = createRouter({
  routeTree,
  context: {
    auth: undefined!, // 型を合わせるためのプレースホルダー
  },
});

declare module "@tanstack/react-router" {
  interface Register {
    router: typeof router;
  }
}

function App() {
  const auth = useAuth();
  return <RouterProvider router={router} context={{ auth }} />;
}
// src/routes/__root.tsx
import { createRootRouteWithContext } from "@tanstack/react-router";

interface RouterContext {
  auth: {
    isAuthenticated: boolean;
    user: { name: string } | null;
  };
}

export const Route = createRootRouteWithContext<RouterContext>()({
  component: RootLayout,
});

この仕組みにより、任意のルートの beforeLoad や loader から context.auth にアクセスでき、認証状態に応じたリダイレクトを型安全に行えます。

型安全なナビゲーション(Link・navigate)

TanStack Router の Link コンポーネントと navigate 関数は、存在するルートのパスしか受け付けません。

import { Link, useNavigate } from "@tanstack/react-router";

// ◎ 型安全:/posts/123 は有効なルートパス
<Link to="/posts/$postId" params={{ postId: "123" }}>
  記事を見る
</Link>

// ◎ 検索パラメータも型付き
<Link to="/posts" search={{ page: 2, sort: "date" }}>
  次のページ
</Link>

// ✗ コンパイルエラー:/post/123 というルートは存在しない
<Link to="/post/$postId" params={{ postId: "123" }}>
  エラー
</Link>
// programmatic navigation
function SomeComponent() {
  const navigate = useNavigate();

  const handleClick = () => {
    navigate({
      to: "/posts/$postId",
      params: { postId: "456" },
      search: { sort: "title" },
    });
  };
}

「リファクタでルートを改名したら Link が全部コンパイルエラーになって気づけた」というのが TanStack Router を使う最大のメリットです。

エラーハンドリングとNotFound

ローダーや beforeLoad でエラーが投げられた場合の処理は errorComponent で定義します。

export const Route = createFileRoute("/posts/$postId")({
  loader: ({ params }) => fetchPost(params.postId),
  errorComponent: ({ error }) => (
    <div>
      <h2>エラーが発生しました</h2>
      <p>{error.message}</p>
    </div>
  ),
  notFoundComponent: () => (
    <div>
      <h2>記事が見つかりません</h2>
    </div>
  ),
  component: PostPage,
});

ルートレベルで errorComponent を設定すると、そのルート以下のすべてのエラーをキャッチするエラーバウンダリとして機能します。

ペンディングコンポーネントとSuspense

ローダーの実行中に表示するコンポーネントは pendingComponent で定義します。

export const Route = createFileRoute("/posts/$postId")({
  loader: ({ params }) => fetchPost(params.postId),
  pendingComponent: () => <div>読み込み中...</div>,
  pendingMs: 300,   // 300ms 以内に完了すれば pending を表示しない
  pendingMinMs: 500, // 表示した場合は最低 500ms 表示する
  component: PostPage,
});

pendingMs と pendingMinMs を使うと、高速なネットワークでは loading UI をフラッシュさせず、遅い場合だけ適切な時間だけ表示するという UX が実現できます。

コードスプリット(遅延ロード)

ルートごとのコードスプリットは lazyRouteComponent または Route.lazy() で行います。

// src/routes/posts/index.tsx
import { createFileRoute, lazyRouteComponent } from "@tanstack/react-router";

export const Route = createFileRoute("/posts/")({
  component: lazyRouteComponent(
    () => import("./PostListPage"),
    "PostListPage"
  ),
});

ファイルベースルーティングを使っている場合は、Vite プラグインが自動的にコードスプリットを適用するため、手動で設定する必要はほぼありません。

実践:検索+ページネーションの実装

型安全な検索パラメータとページネーションを組み合わせた実例です。

// src/routes/posts/index.tsx
import { createFileRoute, Link } from "@tanstack/react-router";
import { z } from "zod";

const searchSchema = z.object({
  page: z.number().int().min(1).catch(1),
  q: z.string().catch(""),
});

export const Route = createFileRoute("/posts/")({
  validateSearch: searchSchema,
  loader: async ({ location: { search } }) => {
    const { page, q } = search;
    const res = await fetch(`/api/posts?page=${page}&q=${q}`);
    return res.json() as Promise<{ posts: Post[]; total: number }>;
  },
  component: PostListPage,
});

function PostListPage() {
  const { posts, total } = Route.useLoaderData();
  const { page, q } = Route.useSearch();
  const navigate = Route.useNavigate();

  return (
    <div>
      <input
        value={q}
        onChange={(e) =>
          navigate({ search: (prev) => ({ ...prev, q: e.target.value, page: 1 }) })
        }
        placeholder="検索..."
      />

      <ul>
        {posts.map((post) => (
          <li key={post.id}>
            <Link to="/posts/$postId" params={{ postId: post.id }}>
              {post.title}
            </Link>
          </li>
        ))}
      </ul>

      <div>
        {page > 1 && (
          <Link search={(prev) => ({ ...prev, page: page - 1 })}>前へ</Link>
        )}
        <span>{page} / {Math.ceil(total / 10)}</span>
        <Link search={(prev) => ({ ...prev, page: page + 1 })}>次へ</Link>
      </div>
    </div>
  );
}

navigate({ search: (prev) => ({ ...prev, ... }) }) の形で現在の検索パラメータをマージしながら更新できます。他のパラメータを消さずに1つだけ変えたいケースで非常に便利です。

React Router v7との使い分けガイド

ユースケース おすすめ
型安全なSPAを作りたい TanStack Router
検索パラメータをスキーマで管理したい TanStack Router
TanStack Queryと組み合わせたい TanStack Router
SSR・サーバー側ルーティングが必要 React Router v7 (Remix)
既存のReact Routerアプリを拡張したい React Router v7
フルスタックフレームワークが欲しい Next.js / SvelteKit / TanStack Start

なお、TanStack Router チームは SSR 対応のフレームワーク TanStack Start を開発中で、2026年後半には安定版が期待されています。Next.js・Remix のような「フルスタック TanStack」が実現する予定です。

DevTools

開発中は TanStack Router DevTools を使うとルートのマッチング状態・ローダーの実行・検索パラメータの中身をリアルタイムで確認できます。

import { TanStackRouterDevtools } from "@tanstack/router-devtools";

// __root.tsx の component に追加
function RootLayout() {
  return (
    <>
      <Outlet />
      {import.meta.env.DEV && <TanStackRouterDevtools />}
    </>
  );
}

まとめ

TanStack Router は「TypeScript で書くなら型が全部通っているべき」という信念を持つ開発者に刺さるライブラリです。

  • ルートパラメータ・検索パラメータ・ローダー戻り値がすべて型推論される
  • Link/navigate に存在しないパスを渡すとコンパイルエラー
  • ファイルベースルーティングとコード生成で型安全を自動維持
  • TanStack Query との組み合わせでデータプリフェッチが楽になる
  • 認証ガード・エラーハンドリング・コードスプリットも標準で対応

React で SPA を作るなら、React Router から乗り換える価値は十分あります。まず npm install @tanstack/react-router で小さなプロジェクトに試してみてください。一度使うと型のない世界には戻れなくなります。

広告・楽天アフィリエイト

ガジェット・オーディオ

イヤホン・便利ガジェットをポイント還元で。

楽天ポイント還元

ガジェットを楽天で見る →