tanstack-router-migration

Compare original and translation side by side

🇺🇸

Original

English
🇨🇳

Translation

Chinese

React Router to TanStack Router Migration

从React Router迁移到TanStack Router

Migrate React applications from React Router to TanStack Router with file-based routing. This skill provides a structured approach for both incremental and clean migrations.
将React应用从React Router迁移到支持基于文件路由的TanStack Router。本技能提供了增量迁移和全新迁移两种结构化方案。

Critical Rules

关键规则

ALWAYS:
  • Use file-based routing with routes in
    src/routes/
    directory
  • Use
    from
    parameter in all hooks for type safety (
    useParams({ from: '/path' })
    )
  • Validate search params with Zod schemas using
    @tanstack/zod-adapter
  • Configure build tool plugin before creating routes
  • Register router type for full TypeScript inference
  • Use
    fallback()
    wrapper for optional search params
NEVER:
  • Edit
    routeTree.gen.ts
    (auto-generated file)
  • Use React Router hooks in new code during migration
  • Forget the
    from
    parameter (loses type safety)
  • Use string-only validation for search params
  • Skip the build plugin configuration
必须遵守:
  • 使用基于文件的路由,路由文件存放于
    src/routes/
    目录
  • 在所有钩子中使用
    from
    参数以确保类型安全(如
    useParams({ from: '/path' })
  • 使用
    @tanstack/zod-adapter
    结合Zod schema验证搜索参数
  • 在创建路由前配置构建工具插件
  • 注册路由类型以实现完整的TypeScript类型推断
  • 对可选搜索参数使用
    fallback()
    包装器
禁止操作:
  • 编辑
    routeTree.gen.ts
    (自动生成的文件)
  • 在迁移期间的新代码中使用React Router钩子
  • 遗漏
    from
    参数(会失去类型安全)
  • 仅使用字符串验证搜索参数
  • 跳过构建插件配置

Dependencies

依赖项

bash
undefined
bash
undefined

Core dependencies

核心依赖

bun add @tanstack/react-router @tanstack/zod-adapter
bun add @tanstack/react-router @tanstack/zod-adapter

Build plugin (choose one based on your bundler)

构建插件(根据你的打包工具选择其一)

bun add -d @tanstack/router-plugin
bun add -d @tanstack/router-plugin

Optional integrations

可选集成

bun add nuqs # URL state management bun add @sentry/react # Error tracking with router integration
undefined
bun add nuqs # URL状态管理 bun add @sentry/react # 集成路由的错误追踪
undefined

Migration Phases

迁移阶段

Phase 1: Assessment

阶段1:评估

Audit existing React Router usage:
bash
undefined
审计现有React Router使用情况:
bash
undefined

Find all React Router imports

查找所有React Router导入

grep -r "from 'react-router" src/ --include=".tsx" --include=".ts" grep -r 'from "react-router' src/ --include=".tsx" --include=".ts"
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/

**Document:**
- [ ] React Router version (v5 or v6)
- [ ] Number of routes
- [ ] `useParams` usage count
- [ ] `useSearchParams` usage count
- [ ] `useNavigate` usage count
- [ ] Custom Link components
- [ ] Route guards/protected routes
- [ ] Existing route structure
grep -r "useParams|useSearchParams|useNavigate|useLocation|useMatch" src/

**记录内容:**
- [ ] React Router版本(v5或v6)
- [ ] 路由数量
- [ ] `useParams`使用次数
- [ ] `useSearchParams`使用次数
- [ ] `useNavigate`使用次数
- [ ] 自定义Link组件
- [ ] 路由守卫/受保护路由
- [ ] 现有路由结构

Phase 2: Setup

阶段2:配置

1. Configure Build Tool
See references/build-configuration.md for full configs.
Rspack/Rsbuild:
typescript
// 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:
typescript
// 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. Configure Linter
jsonc
// biome.jsonc or eslint config
{
  "files": {
    "ignore": ["**/routeTree.gen.ts"]
  },
  "overrides": [
    {
      "include": ["**/routes/**/*"],
      "linter": {
        "rules": {
          "style": {
            "useFilenamingConvention": "off"  // Allow $param.tsx naming
          }
        }
      }
    }
  ]
}
3. Create Routes Directory
bash
mkdir -p src/routes
1. 配置构建工具
完整配置请参考references/build-configuration.md
Rspack/Rsbuild:
typescript
// 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,
        })
      );
      // 防止重建循环
      config.watchOptions = { ignored: ['**/routeTree.gen.ts'] };
      return config;
    },
  },
};
Vite:
typescript
// 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. 配置代码检查工具
jsonc
// biome.jsonc 或 eslint配置
{
  "files": {
    "ignore": ["**/routeTree.gen.ts"]
  },
  "overrides": [
    {
      "include": ["**/routes/**/*"],
      "linter": {
        "rules": {
          "style": {
            "useFilenamingConvention": "off"  // 允许$param.tsx命名方式
          }
        }
      }
    }
  ]
}
3. 创建路由目录
bash
mkdir -p src/routes

Phase 3: Router Creation

阶段3:创建路由

Create Router Instance:
typescript
// 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>
  );
}
Define Router Context Type:
typescript
// src/routes/__root.tsx
import type { QueryClient } from '@tanstack/react-query';

export type RouterContext = {
  basePath: string;
  queryClient: QueryClient;
};
创建路由实例:
typescript
// 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,
});

// 注册路由类型以实现完整的TypeScript类型推断
declare module '@tanstack/react-router' {
  interface Register {
    router: typeof router;
  }

  // 扩展HistoryState以支持类型化导航状态
  interface HistoryState {
    // 在此添加自定义状态属性
    returnUrl?: string;
    documentId?: string;
    documentName?: string;
  }
}

export function App() {
  return (
    <QueryClientProvider client={queryClient}>
      <RouterProvider router={router} />
    </QueryClientProvider>
  );
}
定义路由上下文类型:
typescript
// src/routes/__root.tsx
import type { QueryClient } from '@tanstack/react-query';

export type RouterContext = {
  basePath: string;
  queryClient: QueryClient;
};

Phase 4: Route Migration

阶段4:路由迁移

Create Root Layout:
typescript
// 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" />
      )}
    </>
  );
}
File-Based Route Structure:
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
See references/route-templates.md for complete templates.
创建根布局:
typescript
// 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                    # 根布局
├── index.tsx                     # /(根路径重定向)
├── overview/
│   └── index.tsx                 # /overview
├── topics/
│   ├── index.tsx                 # /topics
│   └── $topicName/
│       ├── index.tsx             # /topics/:topicName
│       └── edit.tsx              # /topics/:topicName/edit
├── security/
│   ├── index.tsx                 # /security(重定向)
│   ├── acls/
│   │   ├── index.tsx             # /security/acls
│   │   ├── create.tsx            # /security/acls/create
│   │   └── $aclName/
│   │       └── details.tsx       # /security/acls/:aclName/details
完整模板请参考references/route-templates.md

Phase 5: Hook Migration

阶段5:钩子迁移

React RouterTanStack Router
useParams()
useParams({ from: '/path/$param' })
useSearchParams()
routeApi.useSearch()
with Zod validation
useNavigate()
useNavigate({ from: '/path' })
useLocation()
useLocation()
(same API)
<Link to="/path">
<Link to="/path">
(type-safe)
<Navigate to="/path" />
<Navigate to="/path" />
See references/migration-patterns.md for detailed before/after examples.
Navigation State:
Pass typed state between routes using
HistoryState
:
typescript
// 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 Migration:
typescript
// 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' });
useSearch with Zod Validation:
typescript
// 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 }) });
  };
}
React RouterTanStack 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
在路由间传递类型化状态:
typescript
// 带状态的导航
const navigate = useNavigate();
navigate({
  to: '/documents/$documentId',
  params: { documentId },
  state: {
    returnUrl: location.pathname,
    documentName: 'My Document',
  },
});

// 在目标组件中读取状态
import { useLocation } from '@tanstack/react-router';

function DocumentPage() {
  const location = useLocation();
  const { returnUrl, documentName } = location.state;
  // 使用状态值...
}
useParams迁移:
typescript
// 迁移前(React Router)
import { useParams } from 'react-router-dom';
const { id } = useParams<{ id: string }>();

// 迁移后(TanStack Router)
import { useParams } from '@tanstack/react-router';
const { id } = useParams({ from: '/items/$id' });
结合Zod验证的useSearch:
typescript
// 在路由文件中
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,
});

// 在组件中
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 }) });
  };
}

Phase 6: Testing

阶段6:测试

Create Test Utilities:
typescript
// 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;
}
Configure Vitest:
typescript
// 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'],
  },
});
创建测试工具:
typescript
// 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:
typescript
// 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'],
  },
});

Phase 7: Integrations

阶段7:集成

Sentry Integration:
typescript
// src/app.tsx
import * as Sentry from '@sentry/react';

Sentry.init({
  dsn: process.env.SENTRY_DSN,
  integrations: [
    Sentry.tanstackRouterBrowserTracingIntegration(router),
  ],
  tracesSampleRate: 1.0,
});
nuqs Integration:
typescript
// src/routes/__root.tsx
import { NuqsAdapter } from 'nuqs/adapters/tanstack-router';

function RootLayout() {
  return (
    <NuqsAdapter>
      <Outlet />
    </NuqsAdapter>
  );
}
Incremental Migration (Legacy Compatibility):
See references/incremental-migration.md for patterns to run both routers together during migration.
Sentry集成:
typescript
// src/app.tsx
import * as Sentry from '@sentry/react';

Sentry.init({
  dsn: process.env.SENTRY_DSN,
  integrations: [
    Sentry.tanstackRouterBrowserTracingIntegration(router),
  ],
  tracesSampleRate: 1.0,
});
nuqs集成:
typescript
// src/routes/__root.tsx
import { NuqsAdapter } from 'nuqs/adapters/tanstack-router';

function RootLayout() {
  return (
    <NuqsAdapter>
      <Outlet />
    </NuqsAdapter>
  );
}
增量迁移(遗留兼容性):
如需在迁移期间同时运行两个路由系统,请参考references/incremental-migration.md中的方案。

Quick Reference

快速参考

Route File Naming

路由文件命名规则

PatternFileURL
Index route
topics/index.tsx
/topics
Dynamic param
topics/$topicName.tsx
/topics/:topicName
Nested dynamic
topics/$topicName/edit.tsx
/topics/:topicName/edit
Pathless layout
_layout.tsx
(no URL segment)
Catch-all
$.tsx
/*
模式文件URL
索引路由
topics/index.tsx
/topics
动态参数
topics/$topicName.tsx
/topics/:topicName
嵌套动态参数
topics/$topicName/edit.tsx
/topics/:topicName/edit
无路径布局
_layout.tsx
(无URL分段)
捕获所有路由
$.tsx
/*

Common Zod Patterns

常见Zod模式

typescript
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),
});
typescript
import { fallback, zodValidator } from '@tanstack/zod-adapter';
import { z } from 'zod';

const searchSchema = z.object({
  // 可选字符串,默认值为undefined
  tab: fallback(z.string().optional(), undefined),

  // 可选数字,带默认值
  page: fallback(z.number().optional(), 1),

  // 必填字符串
  id: z.string(),

  // 枚举类型,带默认值
  sort: fallback(z.enum(['asc', 'desc']).optional(), 'asc'),

  // 布尔类型
  expanded: fallback(z.boolean().optional(), false),
});

Trailing Slash in
from
Parameter

from
参数中的尾部斜杠

The
from
parameter must exactly match the route path as defined:
typescript
// 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
from
参数必须与定义的路由路径完全匹配:
typescript
// 索引路由(文件名为index.tsx)需要包含尾部斜杠:
useParams({ from: '/topics/$topicName/' })  // 路由:topics/$topicName/index.tsx

// 非索引路由不需要包含尾部斜杠:
useParams({ from: '/topics/$topicName/edit' })  // 路由:topics/$topicName/edit.tsx

Type-Safe Navigation

类型安全导航

typescript
// 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' },
});
typescript
// 带参数
<Link to="/topics/$topicName" params={{ topicName: 'my-topic' }}>
  查看主题
</Link>

// 带搜索参数
<Link to="/topics" search={{ page: 2, sort: 'desc' }}>
2</Link>

// 编程式导航
const navigate = useNavigate({ from: '/topics/$topicName' });
navigate({
  to: '/topics/$topicName/edit',
  params: { topicName },
  search: { tab: 'settings' },
});

Checklist

检查清单

Pre-Migration

迁移前

  • Dependencies installed (
    @tanstack/react-router
    ,
    @tanstack/router-plugin
    ,
    @tanstack/zod-adapter
    )
  • Build tool plugin configured
  • Linter configured to allow
    $param.tsx
    naming
  • src/routes/
    directory created
  • 已安装依赖(
    @tanstack/react-router
    @tanstack/router-plugin
    @tanstack/zod-adapter
  • 已配置构建工具插件
  • 已配置代码检查工具以允许
    $param.tsx
    命名方式
  • 已创建
    src/routes/
    目录

Route Migration

路由迁移

  • __root.tsx
    created with providers and layout
  • index.tsx
    created for root redirect
  • All routes migrated to file-based structure
  • Search params validated with Zod schemas
  • staticData
    added for titles/icons
  • 已创建包含提供者和布局的
    __root.tsx
  • 已创建根路径重定向的
    index.tsx
  • 所有路由已迁移到基于文件的结构
  • 已使用Zod schema验证搜索参数
  • 已添加
    staticData
    用于标题/图标

Hook Migration

钩子迁移

  • All
    useParams
    calls updated with
    from
    parameter
  • All
    useSearchParams
    replaced with
    routeApi.useSearch()
  • All
    useNavigate
    calls updated with
    from
    parameter
  • All
    Link
    components verified working
  • 所有
    useParams
    调用已更新为包含
    from
    参数
  • 所有
    useSearchParams
    已替换为
    routeApi.useSearch()
  • 所有
    useNavigate
    调用已更新为包含
    from
    参数
  • 所有
    Link
    组件已验证可正常工作

Testing

测试

  • renderWithFileRoutes
    utility created
  • Vitest configured with TanStack Router plugin
  • Existing tests updated to use new utilities
  • 已创建
    renderWithFileRoutes
    工具
  • 已配置Vitest并集成TanStack Router插件
  • 现有测试已更新为使用新工具

Integrations

集成

  • Sentry integration configured (if used)
  • nuqs adapter wrapped in root layout (if used)
  • 已配置Sentry集成(如果使用)
  • 已在根布局中包装nuqs适配器(如果使用)

Cleanup (after full migration)

清理(完全迁移后)

  • React Router dependencies removed
  • Legacy route definitions deleted
  • BrowserRouter wrapper removed
  • RouterSync component removed
  • 已移除React Router依赖
  • 已删除遗留路由定义
  • 已移除BrowserRouter包装器
  • 已移除RouterSync组件

Common Pitfalls

常见陷阱

  1. Missing
    from
    parameter
    - Always specify
    from
    in hooks for type safety
  2. Forgetting
    fallback()
    wrapper
    - Optional search params need
    fallback(z.string().optional(), undefined)
  3. Trailing slash inconsistency - Configure
    trailingSlash: 'never'
    and be consistent
  4. Editing routeTree.gen.ts - Never edit; it's auto-generated on file changes
  5. Missing build plugin - Routes won't generate without the bundler plugin
  6. Async navigation warnings -
    navigate()
    returns Promise; use
    void navigate()
    or await it
  7. Using
    <Navigate>
    for section redirects
    - Use
    beforeLoad
    with
    throw redirect()
    instead to prevent navigation loops in embedded mode:
    typescript
    beforeLoad: () => {
      throw redirect({ to: '/section/$tab', params: { tab: 'default' }, replace: true });
    }
  8. Trailing slash in
    from
    parameter for index routes
    - Index routes (files named
    index.tsx
    ) require trailing slash in
    from
    :
    typescript
    // Index route: /topics/$topicName/index.tsx
    useParams({ from: '/topics/$topicName/' })  // ✅ Correct (trailing slash)
    useParams({ from: '/topics/$topicName' })   // ❌ Wrong
  9. Missing HistoryState extension - Extend
    HistoryState
    interface for typed navigation state (see Phase 3)
  1. 遗漏
    from
    参数
    - 始终在钩子中指定
    from
    以确保类型安全
  2. 忘记
    fallback()
    包装器
    - 可选搜索参数需要使用
    fallback(z.string().optional(), undefined)
  3. 尾部斜杠不一致 - 配置
    trailingSlash: 'never'
    并保持一致性
  4. 编辑routeTree.gen.ts - 绝对不要编辑该文件,它会随文件变化自动生成
  5. 缺失构建插件 - 没有打包器插件的话,路由无法生成
  6. 异步导航警告 -
    navigate()
    返回Promise,使用
    void navigate()
    或await它
  7. 使用
    <Navigate>
    进行分段重定向
    - 改用
    beforeLoad
    结合
    throw redirect()
    以避免嵌入模式下的导航循环:
    typescript
    beforeLoad: () => {
      throw redirect({ to: '/section/$tab', params: { tab: 'default' }, replace: true });
    }
  8. 索引路由的
    from
    参数尾部斜杠
    - 索引路由(文件名为
    index.tsx
    )的
    from
    参数需要包含尾部斜杠:
    typescript
    // 索引路由:/topics/$topicName/index.tsx
    useParams({ from: '/topics/$topicName/' })  // ✅ 正确(带尾部斜杠)
    useParams({ from: '/topics/$topicName' })   // ❌ 错误
  9. 缺失HistoryState扩展 - 扩展
    HistoryState
    接口以支持类型化导航状态(见阶段3)

Documentation

参考文档