migrate-from-react-router について
React Router v7 から TanStack Router への移行手順をまとめたチェックリストです。ルート、ナビゲーション、データ読み込みの変更と、移行時の注意点を開発者向けに案内します。
主な機能
- 01順序付きの移行チェックリスト
- 02ファイルルートと動的ルートの変換
- 03ナビゲーションとパラメーターの更新
- 04検索パラメーターとローダーの移行
- 05クリーンアップとルートテストの指針
活用シーン
- 01React Router v7 移行の計画
- 02ルート定義とナビゲーションの変換
- 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 v7 | TanStack 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 でアクセス) |