mcpField
M
migrate-from-react-router

migrate-from-react-router

github.com/tanstack開発15.3k181 回インストール4 日前に更新
リポジトリ

migrate-from-react-router について

React Router v7 から TanStack Router への移行手順をまとめたチェックリストです。ルート、ナビゲーション、データ読み込みの変更と、移行時の注意点を開発者向けに案内します。

主な機能

  1. 01順序付きの移行チェックリスト
  2. 02ファイルルートと動的ルートの変換
  3. 03ナビゲーションとパラメーターの更新
  4. 04検索パラメーターとローダーの移行
  5. 05クリーンアップとルートテストの指針

活用シーン

  1. 01React Router v7 移行の計画
  2. 02ルート定義とナビゲーションの変換
  3. 03残存インポートや移行時のエラー確認

インストール

コマンドをコピーしてターミナルで実行します。

$npx skills add https://github.com/tanstack/router --skill migrate-from-react-router

インストール後、Claude Code で/migrate-from-react-routerを入力して読み込まれたか確認します。

詳細

React Router v7 から TanStack Router への移行

これは手順を追った移行チェックリストです。各項目は1つの変換タスクを扱います。順番に完了してください。

CRITICAL:移行後に UI が真っ白な場合は、コンソールを開いてください。「cannot use useNavigate outside of context」のようなエラーは、React Router のインポートが TanStack Router のインポートと併存していることを意味します。react-router(存在する場合は react-router-dom も)をアンインストールして、TypeScript のエラーとして表示されるようにしてください。

CRITICAL:TanStack Router では、ナビゲーションに to と params を使用します。テンプレートリテラルのパスは使用しません。to 文字列に params を埋め込まないでください。

NOTE:React Router v7 では、react-router-dom ではなく react-router からインポートすることが推奨されています。react-router-dom パッケージも引き続き存在しますが、react-router から再エクスポートしているだけです。両方からのインポートを確認してください。

移行前

  • 移行ブランチを作成する
git checkout -b migrate-to-tanstack-router
  • 一時的に React Router と併せて TanStack Router をインストールする
npm install @tanstack/react-router @tanstack/react-router-devtools
npm install -D @tanstack/router-plugin
  • バンドラープラグインを設定する(Vite の例)
// 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({ target: 'react', autoCodeSplitting: true }),
    react(),
  ],
})
  • ルートディレクトリを作成する
mkdir src/routes

ルート定義

  • ルートルートを作成する

React Router:<BrowserRouter> または createBrowserRouter([{ element: <Layout />, children: [...] }])

TanStack Router:src/routes/__root.tsx

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

export const Route = createRootRoute({
  component: () => (
    <>
      <nav>
        <Link to="/">Home</Link>
        <Link to="/about">About</Link>
      </nav>
      <Outlet />
      <TanStackRouterDevtools />
    </>
  ),
})
  • 型登録を行ってルーターインスタンスを作成する
// src/main.tsx
import { StrictMode } 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
  }
}

const rootElement = document.getElementById('root')!
if (!rootElement.innerHTML) {
  ReactDOM.createRoot(rootElement).render(
    <StrictMode>
      <RouterProvider router={router} />
    </StrictMode>,
  )
}
  • 各ルートファイルを createFileRoute に変換する

React Router:

// Defined in route config array
{ path: '/posts', element: <Posts />, loader: postsLoader }

TanStack Router:

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

export const Route = createFileRoute('/posts')({
  loader: async () => {
    const posts = await fetchPosts()
    return { posts }
  },
  component: PostsPage,
})

function PostsPage() {
  const { posts } = Route.useLoaderData()
  return (
    <ul>
      {posts.map((p) => (
        <li key={p.id}>{p.title}</li>
      ))}
    </ul>
  )
}
  • 動的ルートを変換する

React Router:/posts/:postId(コロン構文)

TanStack Router:/posts/$postId(ドル記号構文)

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

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

function PostPage() {
  const { post } = Route.useLoaderData()
  return <article>{post.title}</article>
}

ナビゲーション

  • すべての <Link> コンポーネントを変換する

React Router:

import { Link } from 'react-router'
;<Link to={`/posts/${postId}`}>View Post</Link>

TanStack Router:

import { Link } from '@tanstack/react-router'
;<Link to="/posts/$postId" params={{ postId }}>
  View Post
</Link>

主な違い:

  • to はルートパスのパターンであり、補間された文字列ではありません

  • params は型付きの値を指定する別個のプロパティです

  • アクティブ状態のスタイル設定には、activeProps={{ className: 'font-bold' }} または CSS の data-status="active" 属性を使用します

  • すべての useNavigate 呼び出しを変換する

React Router:

import { useNavigate } from 'react-router'
const navigate = useNavigate()
navigate(`/posts/${postId}`)

TanStack Router:

import { useNavigate } from '@tanstack/react-router'
const navigate = useNavigate()
navigate({ to: '/posts/$postId', params: { postId } })

検索パラメーター

  • useSearchParams を validateSearch と useSearch に置き換える

React Router:

import { useSearchParams } from 'react-router'

function Posts() {
  const [searchParams, setSearchParams] = useSearchParams()
  const page = Number(searchParams.get('page')) || 1

  const goToPage = (p: number) => setSearchParams({ page: String(p) })
}

TanStack Router:

// In the route definition:
import { createFileRoute } from '@tanstack/react-router'
import { z } from 'zod'

export const Route = createFileRoute('/posts')({
  validateSearch: z.object({
    page: z.number().default(1).catch(1),
  }),
  component: Posts,
})

// In the component:
import { useNavigate } from '@tanstack/react-router'

function Posts() {
  const { page } = Route.useSearch()
  const navigate = useNavigate({ from: '/posts' })

  const goToPage = (p: number) => {
    navigate({ search: (prev) => ({ ...prev, page: p }) })
  }
}

主な違い:

  • 検索パラメーターはルートレベルで検証され、型付けされます
  • useSearch() は URLSearchParams ではなく、型付きオブジェクトを返します
  • 他のパラメーターを保持するには、関数形式で更新します

パスパラメーター

  • useParams に from プロパティを追加する

React Router:

import { useParams } from 'react-router'
const { postId } = useParams()

TanStack Router:

import { useParams } from '@tanstack/react-router'
const { postId } = useParams({ from: '/posts/$postId' })

または、ルートコンポーネント内で使用する場合:

const { postId } = Route.useParams()

useLocation — よくある落とし穴

  • useLocation を用途に応じたフックに置き換える

React Router の useLocation は広く使われています。TanStack Router にも同名のフックがありますが、両者は同等ではありません。TanStack Router の useLocation() はルーターの現在のロケーションを返します。ナビゲーションが保留中の場合、現在レンダリングされている内容とは異なることがあります。React Router での useLocation の使用の大半は、より用途に特化したフックに置き換えてください。詳しくは #3110 を参照してください。

実際に必要なものに応じて置き換えます:

// React Router
import { useLocation } from 'react-router'
const location = useLocation()

// ❌ DON'T just swap to TanStack Router's useLocation — it's the "live" URL
import { useLocation } from '@tanstack/react-router'

// ✅ DO use the specific hook for what you need:
import {
  useMatch,
  useMatches,
  useParams,
  useSearch,
} from '@tanstack/react-router'

// Current route match (replaces most useLocation().pathname usage)
const match = useMatch({ from: '/posts/$postId' })

// All active matches (replaces useLocation for breadcrumbs/analytics)
const matches = useMatches()

// Path params (replaces useLocation + manual parsing)
const { postId } = useParams({ from: '/posts/$postId' })

// Search params (replaces useLocation().search parsing)
const { page } = useSearch({ from: '/posts' })

Outlet

  • React Router の Outlet を TanStack Router の Outlet に置き換える

API は同一です。インポートを変更するだけです:

// Before
import { Outlet } from 'react-router'

// After
import { Outlet } from '@tanstack/react-router'

ローダー

  • React Router のローダーを変換する

React Router(v7):

export async function loader({ params }) {
  const post = await fetchPost(params.postId)
  return { post }
}

export default function Post() {
  const { post } = useLoaderData()
  return <div>{post.title}</div>
}

TanStack Router:

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

function Post() {
  const { post } = Route.useLoaderData()
  return <div>{post.title}</div>
}

主な違い:

  • ローダーは独立したエクスポートではなく、ルートオプションです
  • useLoaderData() は Route.useLoaderData()(または useLoaderData({ from }))経由で呼び出します
  • TanStack Router のローダーは、デフォルトではクライアント上で実行されます(サーバー専用ではありません)
  • json() ラッパーは不要です。通常のオブジェクトを返します

コード分割

  • 遅延ルートインポートを変換する — プラグイン設定で autoCodeSplitting: true にすると、自動的に行われます。手動で分割する場合は、.lazy.tsx ファイルを使用します:
// src/routes/lazy-page.lazy.tsx
import { createLazyFileRoute } from '@tanstack/react-router'

export const Route = createLazyFileRoute('/lazy-page')({
  component: () => <div>Lazy loaded</div>,
})

クリーンアップ

  • React Router を削除して検証する
npm uninstall react-router react-router-dom
grep -r "from 'react-router" src/  # find stale imports
npx tsc --noEmit                    # verify clean build
  • すべてのルートをテストする — レンダリング、ナビゲーション(戻る/進むを含む)、検索パラメーター、動的パラメーター、ローダーを検証します

よくある間違い

1. 重大:TanStack Router と併せて React Router のインポートを残す

両方のライブラリは Link、useNavigate、Outlet などをエクスポートしています。React Router のインポートが残っていると、誤ったコンテキストプロバイダーが使われ、「コンテキスト外で useNavigate を使用できません」というエラーが発生します。

// WRONG — mixed imports
import { Link } from '@tanstack/react-router'
import { useNavigate } from 'react-router' // <- still React Router!

// CORRECT — all from TanStack Router
import { Link, useNavigate } from '@tanstack/react-router'

修正方法:react-router/react-router-dom を完全にアンインストールします。TypeScript が古いインポートをすべて検出します。

2. 重大:React Router の useSearchParams パターンを使う
// WRONG — React Router pattern, returns URLSearchParams
const [searchParams, setSearchParams] = useSearchParams()
const page = Number(searchParams.get('page'))

// CORRECT — TanStack Router pattern, returns typed object
// Route definition:
validateSearch:
  z.object({
    page: z.number().default(1).catch(1),
  }),

// Component:
const { page } = Route.useSearch()
// page is already typed as number — no casting needed
3. 重大:パラメーターを to 文字列に埋め込む
// WRONG — React Router habit
<Link to={`/posts/${postId}`}>Post</Link>

// CORRECT — TanStack Router: path pattern + params prop
<Link to="/posts/$postId" params={{ postId }}>Post</Link>
4. 中程度:$param ではなく :param 構文を使う
React Router: /posts/:postId
TanStack Router: /posts/$postId

ファイル名にも $ を使います:src/routes/posts/$postId.tsx

クイックリファレンス:API マッピング

React Router v7TanStack Router
<BrowserRouter><RouterProvider router={router} />
<Routes> / <Route>ファイルベース:src/routes/*.tsx
<Link to="/path"><Link to="/path">
<Link to={/posts/${id}}><Link to="/posts/$postId" params={{ postId: id }}>
useNavigate()('/path')navigate({ to: '/path' })
useParams()useParams({ from: '/route/$param' })
useSearchParams()validateSearch + useSearch({ from })
useLoaderData()Route.useLoaderData()
useLocation()useMatch、useMatches、useParams、useSearch
<Outlet /><Outlet />
loader({ params })loader: ({ params }) => ...(ルートオプション)
action({ request })ミューテーション/フォームライブラリを使用する
lazy(() => import(...))autoCodeSplitting または .lazy.tsx ファイル
:paramName$paramName
*(スプラット)$(スプラット。_splat でアクセス)

コメント