🗺️

React Router v6 から Tanstack Router v1に移行して感じたメリット

に公開

作成日時: 2025-11-19

はじめに

TanStack Routerは、型安全性を重視した次世代のReactルーターで、ファイルベースルーティング、自動コード分割、完全な型推論など、モダンなReactアプリケーション開発に必要な機能を提供しています。

この記事では、実際にReact Router v6からTanStack Router v1へ移行した際に感じたメリットについて、具体的なコード例とともに紹介します。

なお、この記事内で特に断りがない限り、React Routerはv6、TanStack Routerはv1を指しています。

Automatic Code Splitting

TanStack Routerは、ルート単位での自動コード分割をサポートしています。React Routerでコード分割を実現するには、React.lazySuspenseを手動で設定する必要がありましたが、TanStack Routerでは設定ファイルに記述するだけで自動的に処理されます。

React Router の場合

const Home = React.lazy(() => import('./pages/Home'));
const About = React.lazy(() => import('./pages/About'));

function App() {
  return (
    <Suspense fallback={<div>Loading...</div>}>
      <Routes>
        <Route path="/" element={<Home />} />
        <Route path="/about" element={<About />} />
      </Routes>
    </Suspense>
  );
}

TanStack Router (File-Based Routing) の場合

vite.config.ts
import { tanstackRouter } from '@tanstack/router-plugin/vite'

export default defineConfig({
  plugins: [
    tanstackRouter({
      target: 'react',
      autoCodeSplitting: true,
    }),
  ],
});

この設定により、各ルートのコンポーネントが自動的に分割され、必要になったタイミングで読み込まれます。

File-Based Routing

TanStack Routerは、ファイルベースルーティング、コードベースルーティングどちらにも対応しており、今回の移行ではファイルベースルーティングを採用しました。React Routerのコードベースルーティングでは、ルーティングがネストしている場合、ページのURLから該当のコンポーネントを辿るのに少々難があるという課題がありました。ファイルベースルーティングを採用することで、URLとファイルのパスが一致するため、この課題が解決しました。

React Router の場合

React Routerでは、ルート定義をコードで明示的に記述する必要があります。

<Routes>
  <Route path="/" element={<Root />}>
    <Route index element={<Home />} />
    <Route path="about" element={<About />} />
    <Route path="posts" element={<Posts />} />
    <Route path="posts/:postId" element={<Post />} />
    <Route path="users/:userId/profile" element={<UserProfile />} />
  </Route>
</Routes>

TanStack Router の場合

TanStack Routerでは、ファイルシステムの構造から自動的にルート定義を生成できます。移行したプロジェクトではviteを利用しているため、こちらの設定をすることで、vite devで開発サーバを起動しておくと、src/routes/配下の変更を検知して自動的にルート定義ファイルを生成してくれるようになります。

ディレクトリ構造の設計については公式ドキュメントに詳しくルールが記載されているので、そちらを参照すれば問題なくできたのですが、1点だけハマった点があるので紹介します。

それはindexの扱いです。公式ドキュメントではルートのパスにposts.tsxabout.tsxの例があったので、同じように設計していたのですが、ネストされたパスを実装した時に意図しないコンポーネントアウトプットになっていました。少し分かりづらいので、indexの有無によるディレクトリ構造を例示しながら説明します。

ディレクトリ構造の例 (index無し)

src/routes/
├── __root.tsx              # <Root>
├── index.tsx               # <Root><RootIndex>
├── posts.tsx               # <Root><Posts>
├── posts.$postId.tsx       # <Root><Posts><Post>
└── posts.$postId.edit.tsx  # <Root><Posts><Post><EditPost>

ディレクトリ構造の例 (index有り)

src/routes/
├── __root.tsx                    # <Root>
├── index.tsx                     # <Root><RootIndex>
├── posts.index.tsx               # <Root><PostsIndex>
├── posts.$postId.index.tsx       # <Root><PostIndex>
└── posts.$postId.edit.index.tsx  # <Root><EditPostIndex>

最初はindex無しのディレクトリ構造で設計していたのですが、コンポーネントアウトプットを見ると、他のルートがレイアウトとして振る舞っていることに気づきました。なのでレイアウト以外には必ずindexを付ける設計に変更しました。

Routeの型付け

TanStack Routerでは、Path、Path params、Search paramsの全てにおいて型付けが可能です。これにより、ルーティングに関連する全ての要素で型安全性が保証され、コンパイル時にエラーを検出できるようになります。

Pathの型付け

TanStack Routerでは、ルートパスに対して完全な型推論が効きます。これにより、存在しないパスへのナビゲーションをコンパイル時に検出できます。

React Router の場合

import { useNavigate } from "react-router-dom";

function MyComponent() {
  const navigate = useNavigate();

  // タイポしても気づけない
  navigate("/posst/123"); // コンパイルエラーにならない
}

TanStack Router の場合

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

function MyComponent() {
  const navigate = useNavigate();

  // タイポするとコンパイルエラー
  navigate({ to: "/posst/$postId", params: { postId: "123" } }); // エラー: '/posst/$postId' は存在しません

  // 正しいパスのみ許可される
  navigate({ to: "/posts/$postId", params: { postId: "123" } }); // OK
}

自動生成された型定義により、存在するルートパスのみが補完され、タイポや存在しないパスへの遷移を防げます。

Path paramsの型付け

React Routerで、URL(/posts/:postId)からパラメータ:postIdを取得する場合は以下のように書きます。

const { postId } = useParams();

このときpostIdstring | undefined型となり、型付けもされていないため補完が効きません。

TanStack Routerの場合は、

export const Route = createFileRoute("/posts/$postId/")({
  component: Post,
}); // 自動生成

function Post() {
  const { postId } = Route.useParams();
}

と書けて、このときpostIdstringになります。またzodを利用すると、postIdnumberでvalidateすることもできます。

export const Route = createFileRoute("/posts/$postId/")({
  component: Post,
  params: {
    parse: (params) => ({ postId: z.coerce.number().parse(params.postId) }),
  },
});

function Post() {
  const { postId } = Route.useParams();
}

この時、postIdnumber型となり、パースに失敗するとエラーがthrowされます。

Search paramsの型付け

クエリパラメータ(Search params)についても、TanStack Routerでは完全な型安全性が提供されます。

React Router の場合

import { useSearchParams } from "react-router-dom";

function PostList() {
  const [searchParams] = useSearchParams();

  // 型が付いていないため、補完が効かない
  const page = searchParams.get("page"); // string | null
  const sort = searchParams.get("sort"); // string | null
}

TanStack Router の場合

export const Route = createFileRoute("/posts/")({
  component: PostList,
  validateSearch: z.object({
    page: z.number().optional().default(1),
    sort: z.enum(["asc", "desc"]).optional().default("desc"),
  }),
});

function PostList() {
  const { page, sort } = Route.useSearch();
  // page: number
  // sort: 'asc' | 'desc'
}

zodスキーマを使ってバリデーションと型定義を同時に行えるため、不正なクエリパラメータを防ぎつつ、型安全なコードが書けます。また、デフォルト値も設定できるため、undefinedチェックが不要になります。

URL作成の簡略化

TanStack Routerでは、URLを文字列として組み立てる必要がなく、オブジェクト形式で型安全にURLを構築できます。

React Router の場合

import { Link } from 'react-router-dom';

function PostCard({ postId }: { postId: number }) {
  // 文字列として組み立てる必要がある
  return (
    <Link to={`/posts/${postId}?sort=desc&page=1`}>
      View Post
    </Link>
  );
}

TanStack Router の場合

import { Link } from '@tanstack/react-router';

function PostCard({ postId }: { postId: number }) {
  // オブジェクト形式で型安全に記述
  return (
    <Link
      to="/posts/$postId"
      params={{ postId }}
      search={{ sort: 'desc', page: 1 }}
    >
      View Post
    </Link>
  );
}

パラメータとクエリパラメータが明確に分離され、型チェックも効くため、タイポやパラメータの渡し忘れを防げます。また、URLエンコードなども自動的に処理されます。

Data Loading

TanStack Routerには、ルートの読み込み前に実行されるbeforeLoadloaderフックがあります。これを使うと、認証チェックやデータの事前読み込みなどを型安全に実装できます。

認証チェックの例

export const Route = createFileRoute("/dashboard/")({
  component: Dashboard,
  beforeLoad: async ({ context }) => {
    // 認証チェック
    if (!context.auth.isAuthenticated) {
      throw redirect({ to: "/login" });
    }
  },
});

データの事前読み込み

export const Route = createFileRoute("/posts/$postId/")({
  component: Post,
  loader: async ({ params }) => {
    const post = await fetchPost(params.postId);
    return { post };
  },
});

function Post() {
  const { post } = Route.useLoaderData();
  // post のデータが保証されている
}

React Routerのloaderに似た機能ですが、TanStack Routerでは型推論が効くため、返り値の型がコンポーネント側で自動的に推論されます。これにより、データの存在が保証され、安全にデータを扱えます。

まとめ

React Router v6からTanStack Router v1への移行で感じた主なメリットをまとめます。

型安全性の大幅な向上

  • パスの型チェック: 存在しないルートへの遷移をコンパイル時に検出
  • パラメータの型推論: Path paramsとSearch paramsが完全に型付けされる
  • データの型保証: loaderの返り値がコンポーネントで自動推論される

開発体験の改善

  • ファイルベースルーティング: ファイル構造がそのままルート構造になり、直感的
  • 自動コード分割: 簡単な設定だけで各ルートが自動的に分割される
  • URL構築の簡略化: オブジェクト形式で型安全にURLを作成できる

TypeScriptとの親和性

TanStack Routerは、TypeScriptファーストで設計されており、型推論が非常に強力です。これにより、コード補完が効きやすく、リファクタリングも安全に行えます。

React Routerも優れたライブラリですが、TypeScriptを使ったプロジェクトで型安全性を重視するなら、TanStack Routerへの移行を検討する価値は十分にあると思います。

GENDA

Discussion