tanstack-router-migration
Compare original and translation side by side
🇺🇸
Original
English🇨🇳
Translation
ChineseReact 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 directory
src/routes/ - Use parameter in all hooks for type safety (
from)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 wrapper for optional search params
fallback()
NEVER:
- Edit (auto-generated file)
routeTree.gen.ts - Use React Router hooks in new code during migration
- Forget the parameter (loses type safety)
from - Use string-only validation for search params
- Skip the build plugin configuration
必须遵守:
- 使用基于文件的路由,路由文件存放于目录
src/routes/ - 在所有钩子中使用参数以确保类型安全(如
from)useParams({ from: '/path' }) - 使用结合Zod schema验证搜索参数
@tanstack/zod-adapter - 在创建路由前配置构建工具插件
- 注册路由类型以实现完整的TypeScript类型推断
- 对可选搜索参数使用包装器
fallback()
禁止操作:
- 编辑(自动生成的文件)
routeTree.gen.ts - 在迁移期间的新代码中使用React Router钩子
- 遗漏参数(会失去类型安全)
from - 仅使用字符串验证搜索参数
- 跳过构建插件配置
Dependencies
依赖项
bash
undefinedbash
undefinedCore 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
undefinedbun add nuqs # URL状态管理
bun add @sentry/react # 集成路由的错误追踪
undefinedMigration Phases
迁移阶段
Phase 1: Assessment
阶段1:评估
Audit existing React Router usage:
bash
undefined审计现有React Router使用情况:
bash
undefinedFind 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 structuregrep -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/routes1. 配置构建工具
完整配置请参考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/routesPhase 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/detailsSee 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 Router | TanStack Router |
|---|---|
| |
| |
| |
| |
| |
| |
See references/migration-patterns.md for detailed before/after examples.
Navigation State:
Pass typed state between routes using :
HistoryStatetypescript
// 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 Router | TanStack Router |
|---|---|
| |
| 结合Zod验证的 |
| |
| |
| |
| |
详细的前后对比示例请参考references/migration-patterns.md。
导航状态:
使用在路由间传递类型化状态:
HistoryStatetypescript
// 带状态的导航
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
路由文件命名规则
| Pattern | File | URL |
|---|---|---|
| Index route | | |
| Dynamic param | | |
| Nested dynamic | | |
| Pathless layout | | (no URL segment) |
| Catch-all | | |
| 模式 | 文件 | URL |
|---|---|---|
| 索引路由 | | |
| 动态参数 | | |
| 嵌套动态参数 | | |
| 无路径布局 | | (无URL分段) |
| 捕获所有路由 | | |
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
fromfrom
参数中的尾部斜杠
fromThe parameter must exactly match the route path as defined:
fromtypescript
// 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.tsxfromtypescript
// 索引路由(文件名为index.tsx)需要包含尾部斜杠:
useParams({ from: '/topics/$topicName/' }) // 路由:topics/$topicName/index.tsx
// 非索引路由不需要包含尾部斜杠:
useParams({ from: '/topics/$topicName/edit' }) // 路由:topics/$topicName/edit.tsxType-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 naming
$param.tsx - directory created
src/routes/
- 已安装依赖(、
@tanstack/react-router、@tanstack/router-plugin)@tanstack/zod-adapter - 已配置构建工具插件
- 已配置代码检查工具以允许命名方式
$param.tsx - 已创建目录
src/routes/
Route Migration
路由迁移
- created with providers and layout
__root.tsx - created for root redirect
index.tsx - All routes migrated to file-based structure
- Search params validated with Zod schemas
- added for titles/icons
staticData
- 已创建包含提供者和布局的
__root.tsx - 已创建根路径重定向的
index.tsx - 所有路由已迁移到基于文件的结构
- 已使用Zod schema验证搜索参数
- 已添加用于标题/图标
staticData
Hook Migration
钩子迁移
- All calls updated with
useParamsparameterfrom - All replaced with
useSearchParamsrouteApi.useSearch() - All calls updated with
useNavigateparameterfrom - All components verified working
Link
- 所有调用已更新为包含
useParams参数from - 所有已替换为
useSearchParamsrouteApi.useSearch() - 所有调用已更新为包含
useNavigate参数from - 所有组件已验证可正常工作
Link
Testing
测试
- utility created
renderWithFileRoutes - 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
常见陷阱
- Missing parameter - Always specify
fromin hooks for type safetyfrom - Forgetting wrapper - Optional search params need
fallback()fallback(z.string().optional(), undefined) - Trailing slash inconsistency - Configure and be consistent
trailingSlash: 'never' - Editing routeTree.gen.ts - Never edit; it's auto-generated on file changes
- Missing build plugin - Routes won't generate without the bundler plugin
- Async navigation warnings - returns Promise; use
navigate()or await itvoid navigate() - Using for section redirects - Use
<Navigate>withbeforeLoadinstead to prevent navigation loops in embedded mode:throw redirect()typescriptbeforeLoad: () => { throw redirect({ to: '/section/$tab', params: { tab: 'default' }, replace: true }); } - Trailing slash in parameter for index routes - Index routes (files named
from) require trailing slash inindex.tsx:fromtypescript// Index route: /topics/$topicName/index.tsx useParams({ from: '/topics/$topicName/' }) // ✅ Correct (trailing slash) useParams({ from: '/topics/$topicName' }) // ❌ Wrong - Missing HistoryState extension - Extend interface for typed navigation state (see Phase 3)
HistoryState
- 遗漏参数 - 始终在钩子中指定
from以确保类型安全from - 忘记包装器 - 可选搜索参数需要使用
fallback()fallback(z.string().optional(), undefined) - 尾部斜杠不一致 - 配置并保持一致性
trailingSlash: 'never' - 编辑routeTree.gen.ts - 绝对不要编辑该文件,它会随文件变化自动生成
- 缺失构建插件 - 没有打包器插件的话,路由无法生成
- 异步导航警告 - 返回Promise,使用
navigate()或await它void navigate() - 使用进行分段重定向 - 改用
<Navigate>结合beforeLoad以避免嵌入模式下的导航循环:throw redirect()typescriptbeforeLoad: () => { throw redirect({ to: '/section/$tab', params: { tab: 'default' }, replace: true }); } - 索引路由的参数尾部斜杠 - 索引路由(文件名为
from)的index.tsx参数需要包含尾部斜杠:fromtypescript// 索引路由:/topics/$topicName/index.tsx useParams({ from: '/topics/$topicName/' }) // ✅ 正确(带尾部斜杠) useParams({ from: '/topics/$topicName' }) // ❌ 错误 - 缺失HistoryState扩展 - 扩展接口以支持类型化导航状态(见阶段3)
HistoryState