Gemcook Tech Blog
🧭

Next.js経験者のためのTanStack Router入門 ─ 型安全なルーティングの世界へ

に公開

みなさん、Reactアプリケーションのルーティングにはどのライブラリを使用していますか?
Next.jsのファイルベースルーティングを利用している方が多いかと思いますが、最近注目されているルーティングライブラリとしてTanStack Routerがあります。

TanStack Routerは、TanStack Query(旧React Query)やTanStack Tableなどを開発しているTanStackチームが提供する型安全なルーティングライブラリです。
ファイルベースルーティング、検索パラメータの型安全な管理、自動コード分割など、モダンなReactアプリケーション開発に必要な機能を備えています。

https://tanstack.com/router/latest

TanStack Routerとは

TanStack Routerは、クライアントファーストで設計されたReact(およびSolid)向けのルーティングライブラリです。
ViteをはじめRspack/Rsbuild、Webpackなど複数のバンドラーをサポートしており、SPA(シングルページアプリケーション)のルーティングを型安全かつ高パフォーマンスに実現します。

主な特徴として以下の4つが挙げられます。

1. 100%推論されるTypeScriptの型安全性

TanStack Routerの最大の特徴は、ルーティング全体での型安全性です。
パスパラメータ、検索パラメータ、ローダーのデータ、ナビゲーションなど、すべてがTypeScriptで型付けされており、自動的に型推論が行われます。

例えば、Next.jsでは動的ルートのパラメータは以下のように取得しますが、TanStack Routerほどルーティング全体での型連携は強くなく、実行時のバリデーションは自分で行う必要があります。

// Next.js(App Router)
// app/posts/[postId]/page.tsx
export default function PostPage({ params }: { params: { postId: string } }) {
  // params.postId は string型だが、実行時のバリデーションは自分で行う必要がある
  return <div>Post: {params.postId}</div>
}

一方、TanStack Routerでは型が自動的に推論されます。

// TanStack Router
// src/routes/posts.$postId.tsx
import { createFileRoute } from '@tanstack/react-router'

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

function PostPage() {
  const { postId } = Route.useParams()
  // postId は自動的に string型として推論される
  // 存在しないパラメータを参照すると、コンパイル時にエラーになる
  return <div>Post: {postId}</div>
}

さらに、<Link>コンポーネントでのナビゲーションでも型安全性が発揮されます。

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

// ✅ 正しいパスとパラメータ → コンパイルOK
<Link to="/posts/$postId" params={{ postId: '123' }}>記事を見る</Link>

// ❌ 存在しないルートを指定 → コンパイルエラー
<Link to="/not-exist">存在しないページ</Link>

// ❌ 必要なパラメータが不足 → コンパイルエラー
<Link to="/posts/$postId">パラメータ忘れ</Link>

Next.jsでは<Link href="/posts/123">のように文字列でパスを指定するため、タイポや存在しないルートへのリンクはランタイムまで気づけません。
TanStack Routerではこれらがコンパイル時にエラーとして検出されるため、開発中にバグを防ぐことができます。

2. ファーストクラスの検索パラメータ管理

TanStack Routerの大きな強みの1つが、検索パラメータ(URLクエリパラメータ)の型安全な管理です。

Next.jsで検索パラメータを扱う場合、useSearchParamsを使いますが、すべてがstring | nullとして扱われます。

// Next.js
'use client'
import { useSearchParams } from 'next/navigation'

export default function SearchPage() {
  const searchParams = useSearchParams()
  const page = searchParams.get('page') // string | null
  const sort = searchParams.get('sort') // string | null
  // 型変換やバリデーションは自分で行う必要がある
  const pageNum = Number(page) || 1
}

TanStack Routerでは、検索パラメータにスキーマを定義でき、バリデーション付きの型安全な管理が可能です。

// TanStack Router
import { createFileRoute } from '@tanstack/react-router'
import { zodValidator } from '@tanstack/zod-adapter'
import { z } from 'zod'

// URLの検索パラメータは文字列で渡されるため、
// z.coerce.number() や fallback() を使ってパース・フォールバックを明示するのが安全です
const searchSchema = z.object({
  page: z.coerce.number().default(1),
  sort: z.enum(['asc', 'desc']).default('desc'),
  filter: z.string().optional(),
})

export const Route = createFileRoute('/search')({
  validateSearch: zodValidator(searchSchema),
  component: SearchPage,
})

function SearchPage() {
  const { page, sort, filter } = Route.useSearch()
  // page: number(デフォルト値付き)
  // sort: 'asc' | 'desc'(デフォルト値付き)
  // filter: string | undefined
  return <div>Page: {page}, Sort: {sort}</div>
}

検索パラメータの更新もタイプセーフに行うことができます。

import { useNavigate } from '@tanstack/react-router'

function Pagination() {
  const navigate = useNavigate()

  return (
    <button
      onClick={() =>
        navigate({
          search: (prev) => ({ ...prev, page: prev.page + 1 }),
        })
      }
    >
      次のページ
    </button>
  )
}

URLの検索パラメータがアプリケーション内で状態管理ツールのように使えるのは非常に強力なポイントです。

3. ファイルベースルーティング

TanStack RouterもNext.jsと同様にファイルベースでルートを定義することができます。
ただし、ルートの定義方法にはいくつかの違いがあります。

Next.js(App Router)とTanStack Routerのファイル構成の比較は以下の通りです。

機能 Next.js(App Router) TanStack Router
ルートファイルの場所 app/ ディレクトリ src/routes/ ディレクトリ
ページファイル名 page.tsx index.tsx または route.tsx
レイアウトファイル layout.tsx route.tsx(親ルート)
動的ルート [param] $param
ネスト表現 ディレクトリのネスト ディレクトリ または .区切りのフラットファイル
ルートグループ (group) (folder)
パスレスルート なし _foo

TanStack Routerの特徴的なポイントとして、フラットルートとディレクトリルートを混在させることができる点があります。

// ディレクトリ形式
src/routes/
├── __root.tsx
├── index.tsx
├── posts/
│   ├── index.tsx
│   └── $postId.tsx
└── about.tsx

// フラット形式(.区切り)
src/routes/
├── __root.tsx
├── index.tsx
├── posts.index.tsx
├── posts.$postId.tsx
└── about.tsx

上記の2つは同じルート構造を表しています。プロジェクトの規模や好みに応じて使い分けることができるのは柔軟性のあるポイントです。

4. データローディングと組み込みキャッシュ

TanStack Routerにはルートごとにデータを読み込むためのloaderAPIが組み込まれています。

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

export const Route = createFileRoute('/posts/')({
  loader: async () => {
    const res = await fetch('/api/posts')
    return res.json()
  },
  component: PostsPage,
})

function PostsPage() {
  const posts = Route.useLoaderData()
  // posts は loader の返り値から自動的に型推論される
  return (
    <ul>
      {posts.map((post) => (
        <li key={post.id}>{post.title}</li>
      ))}
    </ul>
  )
}

さらに、TanStack Routerには軽量な組み込みキャッシュが搭載されており、staleTimeを設定することで同じデータの再フェッチを防ぐことができます。

const router = createRouter({
  routeTree,
  defaultPreload: 'intent', // リンクにホバーした時点でプリロード
  defaultStaleTime: 5000,  // 5秒間はキャッシュを利用
})

より高度なキャッシュ管理が必要な場合は、TanStack Queryとの組み合わせも公式でサポートされているため、段階的にキャッシュ戦略を強化していくことも可能です。

Next.jsとの比較

Next.jsとTanStack Routerの主な違いを比較表でまとめます。

比較項目 Next.js TanStack Router
種類 フルスタックフレームワーク ルーティングライブラリ
レンダリング SSR / SSG / ISR / CSR CSR(SPAがメイン)
ベースツール Webpack / Turbopack Vite
型安全性(ルーティング) 限定的(stringベースのパス指定) 完全な型推論(パス、パラメータ、検索パラメータ)
検索パラメータ useSearchParamsstring | null スキーマベースのバリデーション+型推論
データフェッチング Server Components / Route Handlers loader API + TanStack Query連携
SSR対応 標準搭載 可能(TanStack Start推奨、単体では手動セットアップが必要)
デプロイ先 Vercelに最適化(他も可能) どこでも(Viteベース)
学習コスト 中程度(独自の概念が多い) TanStack Queryを知っていれば低い
エコシステム 非常に大きい 成長中

https://zenn.dev/gemcook/articles/tanstack-start-first

TanStack Routerの始め方

インストール

Viteプロジェクトを前提に、必要なパッケージをインストールします。

npm install @tanstack/react-router
npm install -D @tanstack/router-plugin

Viteプラグインの設定

vite.config.tsにTanStack Routerのプラグインを追加します。

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

export default defineConfig({
  plugins: [
    tanstackRouter(),
    react(),
  ],
})

ルートの作成

src/routes/ディレクトリにルートファイルを作成します。

// src/routes/__root.tsx
import { createRootRoute, Outlet, Link } from '@tanstack/react-router'

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

function RootLayout() {
  return (
    <div>
      <nav>
        <Link to="/">ホーム</Link>
        <Link to="/about">About</Link>
        <Link to="/posts">記事一覧</Link>
      </nav>
      <main>
        <Outlet />
      </main>
    </div>
  )
}
// src/routes/index.tsx
import { createFileRoute } from '@tanstack/react-router'

export const Route = createFileRoute('/')({
  component: HomePage,
})

function HomePage() {
  return <h1>ホームページ</h1>
}
// 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}`)
    return res.json()
  },
  component: PostDetailPage,
})

function PostDetailPage() {
  const post = Route.useLoaderData()
  const { postId } = Route.useParams()
  return (
    <div>
      <h1>{post.title}</h1>
      <p>{post.body}</p>
    </div>
  )
}

Routerの作成とマウント

// 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,
  defaultPreload: 'intent',
  scrollRestoration: true,
})

// 型安全のためのモジュール宣言
declare module '@tanstack/react-router' {
  interface Register {
    router: typeof router
  }
}

// index.html の mount 先と id を合わせてください(公式例では id="root")
const rootElement = document.getElementById('app')!

if (!rootElement.innerHTML) {
  const root = ReactDOM.createRoot(rootElement)
  root.render(<RouterProvider router={router} />)
}

TanStack Routerを使うメリット・デメリット

メリット

型安全性がとにかく強力
ルートのパス、パラメータ、検索パラメータ、ローダーのデータまですべてが型推論されるため、ランタイムエラーを大幅に減らすことができます。Next.jsでは実現できないレベルの型安全性です。

検索パラメータの管理が楽になる
URLの検索パラメータをスキーマ付きで型安全に管理できるため、一覧画面のフィルターやページネーションなどの実装が格段に楽になります。

Viteベースの高速な開発体験
HMRの速さやビルド時間の短さなど、Viteの恩恵をそのまま受けることができます。

段階的な導入が可能
ルーティングライブラリ単体なので、既存のReact SPAプロジェクトに段階的に導入することができます。

デメリット

SSRは手動セットアップが必要
TanStack Router単体でもSSRは可能ですが、手動でのセットアップが必要です。公式が推奨するのはTanStack Startとの組み合わせで、こちらを使うとゼロコンフィグで利用できます。

エコシステムがNext.jsに比べて小さい
Next.jsと比較すると情報量やサードパーティの連携が少ない部分があります。ただし、TanStackエコシステム全体としては急速に成長しています。

学習コスト
Next.jsとは異なる概念(createFileRoutevalidateSearchloaderのAPI設計など)があるため、最初は慣れが必要です。

こんなプロジェクトにおすすめ

TanStack Routerは以下のようなプロジェクトで特に力を発揮します。

TypeScriptを最大限に活かしたいSPAプロジェクト
TanStack RouterはSSRにも対応できますが、単体利用では構成を自分で組み立てる必要があります。SSRやサーバー連携まで含めてスムーズに始めたい場合は、TanStack Startを使うのがわかりやすい選択です。

検索パラメータを多用する管理画面やダッシュボード
フィルター、ソート、ページネーションなどの状態をURLで管理する画面が多い場合に最適です。

Viteを使ったReact SPAの新規開発
Next.jsのSSR機能が不要なプロジェクトであれば、Vite + TanStack Routerの組み合わせは軽量かつ高速な選択肢になります。

まとめ

TanStack Routerは、型安全性検索パラメータ管理の2つの観点でNext.jsのルーティングを大きく上回る体験を提供してくれるライブラリです。
特に、TypeScriptを活用してルーティング周りのバグを減らしたい場合や、URLの検索パラメータを活用した画面を多く作る場合には強力な選択肢になります。

一方で、SSRやフルスタック機能が必要な場合はTanStack Startとの組み合わせを検討する必要があります。Next.jsの置き換えというよりは、プロジェクトの要件に合わせて最適なツールを選ぶことが大切です。

ぜひ一度触ってみて、型安全なルーティングの快適さを体験してみてください!

https://tanstack.com/router/latest/docs/overview

Gemcook Tech Blog
Gemcook Tech Blog

Discussion