关于 migrate-from-react-router
这是一份将应用从 React Router v7 迁移到 TanStack Router 的分步清单,面向负责路由改造的开发者,涵盖路由、导航、数据加载及迁移易错点。
主要功能
- 01按顺序排列的迁移清单
- 02文件路由与动态路由转换
- 03导航及参数更新
- 04搜索参数与加载器迁移
- 05清理与路由测试指引
适用场景
- 01规划 React 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
这是一份循序渐进的迁移检查清单。每项检查对应一项转换任务。请按顺序完成。
CRITICAL:如果迁移后 UI 空白,请打开控制台。出现“cannot use useNavigate outside of context”之类的错误,意味着 React Router 导入仍与 TanStack Router 导入并存。卸载
react-router(以及存在时的react-router-dom),以便将这些问题显示为 TypeScript 错误。CRITICAL:TanStack Router 使用
to+params进行导航,而不是使用模板字面量路径。切勿将参数插入to字符串。NOTE:React Router v7 建议从
react-router导入(而不是react-router-dom)。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' }}或data-status="active"属性来设置 CSS -
转换所有
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>
}
主要区别:
- 加载器是路由选项,而不是单独导出
- 通过
Route.useLoaderData()(或useLoaderData({ from }))调用useLoaderData() - 默认情况下,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 导入会导致“cannot use useNavigate outside of context”错误,因为使用了错误的上下文提供程序。
// 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 访问) |