tanstack-router-migration について
React Router から TanStack Router へアプリを移行する React 開発者向けのスキルです。段階的・漸進的な移行、ファイルベースのルート、型安全なフック、検索パラメーター検証、テストを扱います。
主な機能
- 01段階的な移行ワークフロー
- 02ファイルベースのルート構成
- 03型安全なフックとナビゲーション
- 04Zod による検索パラメーター検証
- 05テスト設定と移行チェック
活用シーン
- 01React Router 移行の計画
- 02ルートの段階的な移行
- 03フックと検索パラメーターの更新
- 04移行後のルートのテスト準備
インストール
コマンドをコピーしてターミナルで実行します。
npx skills add https:/ / github.com/ redpanda-data/ console --skill tanstack-router-migrationインストール後、Claude Code で/tanstack-router-migrationを入力して読み込まれたか確認します。
同梱ファイル
SKILL.md と一緒にインストールされる参考資料やスクリプト。
references/build-configuration.mdincremental-migration.mdmigration-patterns.mdroute-templates.md詳細
React Router から TanStack Router への移行
ファイルベースのルーティングを使用して、React アプリケーションを React Router から TanStack Router に移行します。このスキルでは、段階的な移行と一括移行の両方に対応した体系的な方法を紹介します。
重要なルール
必ず行うこと:
src/routes/ディレクトリ内にルートを配置するファイルベースのルーティングを使用する- 型安全性を確保するため、すべてのフックで
fromパラメーターを使用する(useParams({ from: '/path' })) @tanstack/zod-adapterを使用した Zod スキーマで検索パラメーターを検証する- ルートを作成する前にビルドツールのプラグインを設定する
- TypeScript の推論を完全に有効にするため、ルーターの型を登録する
- 省略可能な検索パラメーターには
fallback()ラッパーを使用する
絶対に行わないこと:
routeTree.gen.tsを編集する(自動生成ファイルです)- 移行中に新しいコードで React Router のフックを使用する
fromパラメーターを省略する(型安全性が失われます)- 検索パラメーターの検証に文字列のみの検証を使用する
- ビルドプラグインの設定を省略する
依存関係
# Core dependencies
bun add @tanstack/react-router @tanstack/zod-adapter
# Build plugin (choose one based on your bundler)
bun add -d @tanstack/router-plugin
# Optional integrations
bun add nuqs # URL state management
bun add @sentry/react # Error tracking with router integration
移行フェーズ
フェーズ 1:評価
既存の React Router の使用状況を監査します:
# Find all React Router imports
grep -r "from 'react-router" src/ --include="*.tsx" --include="*.ts"
grep -r 'from "react-router' src/ --include="*.tsx" --include="*.ts"
# Find hook usages
grep -r "useParams\|useSearchParams\|useNavigate\|useLocation\|useMatch" src/
以下を記録します:
- React Router のバージョン(v5 または v6)
- ルート数
-
useParamsの使用回数 -
useSearchParamsの使用回数 -
useNavigateの使用回数 - カスタム Link コンポーネント
- ルートガード/保護されたルート
- 既存のルート構成
フェーズ 2:セットアップ
1. ビルドツールを設定する
設定全体については、references/build-configuration.md を参照してください。
Rspack/Rsbuild:
// rsbuild.config.ts
import { TanStackRouterRspack } from '@tanstack/router-plugin/rspack';
export default {
tools: {
rspack: (config) => {
config.plugins?.push(
TanStackRouterRspack({
target: 'react',
autoCodeSplitting: true,
routesDirectory: './src/routes',
generatedRouteTree: './src/routeTree.gen.ts',
quoteStyle: 'single',
semicolons: true,
})
);
// Prevent rebuild loop
config.watchOptions = { ignored: ['**/routeTree.gen.ts'] };
return config;
},
},
};
Vite:
// vite.config.ts
import { TanStackRouterVite } from '@tanstack/router-plugin/vite';
export default defineConfig({
plugins: [
TanStackRouterVite({
target: 'react',
autoCodeSplitting: true,
routesDirectory: './src/routes',
generatedRouteTree: './src/routeTree.gen.ts',
}),
react(),
],
});
2. リンターを設定する
// biome.jsonc or eslint config
{
"files": {
"ignore": ["**/routeTree.gen.ts"]
},
"overrides": [
{
"include": ["**/routes/**/*"],
"linter": {
"rules": {
"style": {
"useFilenamingConvention": "off" // Allow $param.tsx naming
}
}
}
}
]
}
3. ルートディレクトリを作成する
mkdir -p src/routes
フェーズ 3:ルーターの作成
ルーターインスタンスを作成する:
// src/app.tsx
import { createRouter, RouterProvider } from '@tanstack/react-router';
import { QueryClient, QueryClientProvider } from '@tanstack/react-query';
import { routeTree } from './routeTree.gen';
import { NotFoundPage } from './components/misc/not-found-page';
const queryClient = new QueryClient();
const router = createRouter({
routeTree,
context: {
basePath: getBasePath(),
queryClient,
},
basepath: getBasePath(),
trailingSlash: 'never',
defaultNotFoundComponent: NotFoundPage,
});
// Register router type for full TypeScript inference
declare module '@tanstack/react-router' {
interface Register {
router: typeof router;
}
// Extend HistoryState for typed navigation state
interface HistoryState {
// Add your custom state properties here
returnUrl?: string;
documentId?: string;
documentName?: string;
}
}
export function App() {
return (
<QueryClientProvider client={queryClient}>
<RouterProvider router={router} />
</QueryClientProvider>
);
}
ルーターのコンテキスト型を定義する:
// src/routes/__root.tsx
import type { QueryClient } from '@tanstack/react-query';
export type RouterContext = {
basePath: string;
queryClient: QueryClient;
};
フェーズ 4:ルートの移行
ルートレイアウトを作成する:
// src/routes/__root.tsx
import { createRootRouteWithContext, Outlet } from '@tanstack/react-router';
import { TanStackRouterDevtools } from '@tanstack/react-router-devtools';
import type { QueryClient } from '@tanstack/react-query';
import { NuqsAdapter } from 'nuqs/adapters/tanstack-router';
export type RouterContext = {
basePath: string;
queryClient: QueryClient;
};
export const Route = createRootRouteWithContext<RouterContext>()({
component: RootLayout,
});
function RootLayout() {
return (
<>
<NuqsAdapter>
<ErrorBoundary>
<AppLayout>
<Outlet />
</AppLayout>
</ErrorBoundary>
</NuqsAdapter>
{process.env.NODE_ENV === 'development' && (
<TanStackRouterDevtools position="bottom-right" />
)}
</>
);
}
ファイルベースのルート構成:
src/routes/
├── __root.tsx # Root layout
├── index.tsx # / (root redirect)
├── overview/
│ └── index.tsx # /overview
├── topics/
│ ├── index.tsx # /topics
│ └── $topicName/
│ ├── index.tsx # /topics/:topicName
│ └── edit.tsx # /topics/:topicName/edit
├── security/
│ ├── index.tsx # /security (redirect)
│ ├── acls/
│ │ ├── index.tsx # /security/acls
│ │ ├── create.tsx # /security/acls/create
│ │ └── $aclName/
│ │ └── details.tsx # /security/acls/:aclName/details
完全なテンプレートについては、references/route-templates.md を参照してください。
フェーズ 5:フックの移行
| React Router | TanStack Router |
|---|---|
useParams() | useParams({ from: '/path/$param' }) |
useSearchParams() | Zod 検証を使った routeApi.useSearch() |
useNavigate() | useNavigate({ from: '/path' }) |
useLocation() | useLocation()(同じ API) |
<Link to="/path"> | <Link to="/path">(型安全) |
<Navigate to="/path" /> | <Navigate to="/path" /> |
変更前と変更後の詳しい例については、references/migration-patterns.md を参照してください。
ナビゲーション状態:
HistoryState を使用して、型付けされた状態をルート間で渡します:
// Navigating with state
const navigate = useNavigate();
navigate({
to: '/documents/$documentId',
params: { documentId },
state: {
returnUrl: location.pathname,
documentName: 'My Document',
},
});
// Reading state in destination component
import { useLocation } from '@tanstack/react-router';
function DocumentPage() {
const location = useLocation();
const { returnUrl, documentName } = location.state;
// Use state values...
}
useParams の移行:
// Before (React Router)
import { useParams } from 'react-router-dom';
const { id } = useParams<{ id: string }>();
// After (TanStack Router)
import { useParams } from '@tanstack/react-router';
const { id } = useParams({ from: '/items/$id' });
Zod 検証を使った useSearch:
// In route file
import { fallback, zodValidator } from '@tanstack/zod-adapter';
import { z } from 'zod';
const searchSchema = z.object({
tab: fallback(z.string().optional(), undefined),
page: fallback(z.number().optional(), 1),
q: fallback(z.string().optional(), undefined),
});
export const Route = createFileRoute('/items/')({
validateSearch: zodValidator(searchSchema),
component: ItemsPage,
});
// In component
import { getRouteApi, useNavigate } from '@tanstack/react-router';
const routeApi = getRouteApi('/items/');
function ItemsPage() {
const { tab, page, q } = routeApi.useSearch();
const navigate = useNavigate({ from: '/items/' });
const handleTabChange = (newTab: string) => {
navigate({ search: (prev) => ({ ...prev, tab: newTab }) });
};
}
フェーズ 6:テスト
テストユーティリティを作成する:
// src/test-utils.tsx
import { createMemoryHistory, createRouter, RouterProvider } from '@tanstack/react-router';
import { QueryClient, QueryClientProvider } from '@tanstack/react-query';
import { render, type RenderOptions } from '@testing-library/react';
import { routeTree } from './routeTree.gen';
import type { RouterContext } from './routes/__root';
interface RenderWithFileRoutesOptions extends Omit<RenderOptions, 'wrapper'> {
initialLocation?: string;
routerContext?: Partial<RouterContext>;
}
export function renderWithFileRoutes(
ui: React.ReactElement | null = null,
{ initialLocation = '/', routerContext = {}, ...renderOptions }: RenderWithFileRoutesOptions = {}
) {
const queryClient = new QueryClient({
defaultOptions: { queries: { retry: false } },
});
const router = createRouter({
routeTree,
history: createMemoryHistory({ initialEntries: [initialLocation] }),
context: { basePath: '', queryClient, ...routerContext },
});
function Wrapper({ children }: { children: React.ReactNode }) {
return (
<QueryClientProvider client={queryClient}>
<RouterProvider router={router}>{children}</RouterProvider>
</QueryClientProvider>
);
}
return {
...render(ui ?? <div />, { wrapper: Wrapper, ...renderOptions }),
router,
};
}
export async function renderRoute(location: string, options?: RenderWithFileRoutesOptions) {
const result = renderWithFileRoutes(null, { initialLocation: location, ...options });
await result.router.load();
return result;
}
Vitest を設定する:
// vitest.config.integration.mts
import { tanstackRouter } from '@tanstack/router-plugin/vite';
export default defineConfig({
plugins: [
tanstackRouter({
target: 'react',
routesDirectory: './src/routes',
generatedRouteTree: './src/routeTree.gen.ts',
}),
react(),
],
test: {
environment: 'jsdom',
setupFiles: ['./src/test/setup.ts'],
},
});
フェーズ 7:統合
Sentry の統合:
// src/app.tsx
import * as Sentry from '@sentry/react';
Sentry.init({
dsn: process.env.SENTRY_DSN,
integrations: [
Sentry.tanstackRouterBrowserTracingIntegration(router),
],
tracesSampleRate: 1.0,
});
nuqs の統合:
// src/routes/__root.tsx
import { NuqsAdapter } from 'nuqs/adapters/tanstack-router';
function RootLayout() {
return (
<NuqsAdapter>
<Outlet />
</NuqsAdapter>
);
}
段階的な移行(レガシーとの互換性):
移行中に両方のルーターを併用するパターンについては、references/incremental-migration.md を参照してください。
クイックリファレンス
ルートファイルの命名
| パターン | ファイル | URL |
|---|---|---|
| インデックスルート | topics/index.tsx | /topics |
| 動的パラメーター | topics/$topicName.tsx | /topics/:topicName |
| ネストされた動的ルート | topics/$topicName/edit.tsx | /topics/:topicName/edit |
| パスレスレイアウト | _layout.tsx | (URL セグメントなし) |
| キャッチオール | $.tsx | /* |
よく使う Zod パターン
import { fallback, zodValidator } from '@tanstack/zod-adapter';
import { z } from 'zod';
const searchSchema = z.object({
// Optional string with undefined default
tab: fallback(z.string().optional(), undefined),
// Optional number with default value
page: fallback(z.number().optional(), 1),
// Required string
id: z.string(),
// Enum with default
sort: fallback(z.enum(['asc', 'desc']).optional(), 'asc'),
// Boolean
expanded: fallback(z.boolean().optional(), false),
});
from パラメーターの末尾のスラッシュ
from パラメーターは、定義されたルートパスと完全に一致させる必要があります:
// Index routes (files named index.tsx) include trailing slash:
useParams({ from: '/topics/$topicName/' }) // Route: topics/$topicName/index.tsx
// Non-index routes do NOT include trailing slash:
useParams({ from: '/topics/$topicName/edit' }) // Route: topics/$topicName/edit.tsx
型安全なナビゲーション
// With params
<Link to="/topics/$topicName" params={{ topicName: 'my-topic' }}>
View Topic
</Link>
// With search params
<Link to="/topics" search={{ page: 2, sort: 'desc' }}>
Page 2
</Link>
// Programmatic navigation
const navigate = useNavigate({ from: '/topics/$topicName' });
navigate({
to: '/topics/$topicName/edit',
params: { topicName },
search: { tab: 'settings' },
});
チェックリスト
移行前
- 依存関係をインストール済み(
@tanstack/react-router、@tanstack/router-plugin、@tanstack/zod-adapter) - ビルドツールのプラグインを設定済み
-
$param.tsx形式の命名を許可するようリンターを設定済み -
src/routes/ディレクトリを作成済み
ルートの移行
- プロバイダーとレイアウトを含む
__root.tsxを作成 - ルートリダイレクト用の
index.tsxを作成 - すべてのルートをファイルベースの構成に移行
- 検索パラメーターを Zod スキーマで検証
- タイトル/アイコン用の
staticDataを追加
フックの移行
- すべての
useParams呼び出しをfromパラメーター付きに更新 - すべての
useSearchParamsをrouteApi.useSearch()に置き換え - すべての
useNavigate呼び出しをfromパラメーター付きに更新 - すべての
Linkコンポーネントの動作を確認
テスト
-
renderWithFileRoutesユーティリティを作成 - TanStack Router プラグインを使用するように Vitest を設定
- 新しいユーティリティを使用するように既存のテストを更新
インテグレーション
- Sentry インテグレーションを設定(使用している場合)
-
nuqsアダプターをルートレイアウトでラップ(使用している場合)
クリーンアップ(完全移行後)
- React Router の依存関係を削除
- レガシーなルート定義を削除
- BrowserRouter ラッパーを削除
- RouterSync コンポーネントを削除
よくある落とし穴
fromパラメーターの指定漏れ - 型安全性を確保するため、フックでは常にfromを指定してくださいfallback()ラッパーの付け忘れ - オプションの検索パラメーターにはfallback(z.string().optional(), undefined)が必要です- 末尾のスラッシュの不統一 -
trailingSlash: 'never'を設定し、一貫性を保ってください routeTree.gen.tsの編集 - ファイルの変更時に自動生成されるため、編集しないでください- ビルドプラグインの設定漏れ - バンドラープラグインがないとルートは生成されません
- 非同期ナビゲーションの警告 -
navigate()は Promise を返します。void navigate()を使用するか、awaitしてください - セクションのリダイレクトに
<Navigate>を使用 - 埋め込みモードでのナビゲーションループを防ぐため、代わりにbeforeLoadでthrow redirect()を使用してください:beforeLoad: () => { throw redirect({ to: '/section/$tab', params: { tab: 'default' }, replace: true }); } - インデックスルートの
fromパラメーターに末尾のスラッシュがない - インデックスルート(ファイル名がindex.tsxのルート)では、fromに末尾のスラッシュが必要です:// Index route: /topics/$topicName/index.tsx useParams({ from: '/topics/$topicName/' }) // ✅ Correct (trailing slash) useParams({ from: '/topics/$topicName' }) // ❌ Wrong - HistoryState 拡張の設定漏れ - 型付きのナビゲーション状態を使用するには、
HistoryStateインターフェースを拡張してください(フェーズ 3 を参照)