※本記事には広告(楽天アフィリエイト)リンクが含まれる場合があります。
「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 で小さなプロジェクトに試してみてください。一度使うと型のない世界には戻れなくなります。