migrate-from-react-router
Compare original and translation side by side
🇺🇸
Original
English🇨🇳
Translation
ChineseMigrate from React Router v7 to TanStack Router
从 React Router v7 迁移到 TanStack Router
This is a step-by-step migration checklist. Each check covers one conversion task. Complete them in order.
CRITICAL: If your UI is blank after migration, open the console. Errors like "cannot use useNavigate outside of context" mean React Router imports remain alongside TanStack Router imports. Uninstall(andreact-routerif present) to surface them as TypeScript errors.react-router-domCRITICAL: TanStack Router uses+tofor navigation, NOT template literal paths. Never interpolate params into theparamsstring.toNOTE: React Router v7 recommends importing from(notreact-router). Thereact-router-dompackage still exists but just re-exports fromreact-router-dom. Check for imports from both.react-router
这是一份分步迁移检查清单,每一项对应一个转换任务,请按顺序完成。
关键提示:如果迁移后 UI 显示空白,请打开控制台。类似「cannot use useNavigate outside of context」的错误说明仍存在 React Router 与 TanStack Router 的导入并存的情况。卸载(如果有react-router也一并卸载),这样这些残留导入会以 TypeScript 错误的形式暴露出来。react-router-dom关键提示:TanStack Router 使用+to进行导航,不使用模板字面量路径。绝对不要将 params 插值到params字符串中。to注意:React Router v7 推荐从导入(而非react-router)。react-router-dom包仍然存在,但只是重新导出react-router-dom的内容。请检查是否存在来自这两个包的导入。react-router
Pre-Migration
迁移前准备
- Create a migration branch
bash
git checkout -b migrate-to-tanstack-router- Install TanStack Router alongside React Router temporarily
bash
npm install @tanstack/react-router @tanstack/react-router-devtools
npm install -D @tanstack/router-plugin- Configure bundler plugin (Vite example)
ts
// 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(),
],
})- Create routes directory
bash
mkdir src/routes- 创建迁移分支
bash
git checkout -b migrate-to-tanstack-router- 临时安装 TanStack Router 与 React Router 共存
bash
npm install @tanstack/react-router @tanstack/react-router-devtools
npm install -D @tanstack/router-plugin- 配置打包器插件(Vite 示例)
ts
// 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(),
],
})- 创建 routes 目录
bash
mkdir src/routesRoute Definitions
路由定义
- Create root route
React Router: or
<BrowserRouter>createBrowserRouter([{ element: <Layout />, children: [...] }])TanStack Router:
src/routes/__root.tsxtsx
// 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 />
</>
),
})- Create router instance with type registration
tsx
// 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>,
)
}- Convert each route file to
createFileRoute
React Router:
tsx
// Defined in route config array
{ path: '/posts', element: <Posts />, loader: postsLoader }TanStack Router:
tsx
// 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>
)
}- Convert dynamic routes
React Router: (colon syntax)
/posts/:postIdTanStack Router: (dollar syntax)
/posts/$postIdtsx
// 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>
}- 创建根路由
React Router: 或
<BrowserRouter>createBrowserRouter([{ element: <Layout />, children: [...] }])TanStack Router:
src/routes/__root.tsxtsx
// 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 />
</>
),
})- 创建带类型注册的路由实例
tsx
// 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:
tsx
// Defined in route config array
{ path: '/posts', element: <Posts />, loader: postsLoader }TanStack Router:
tsx
// 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/:postIdTanStack Router:(美元符号语法)
/posts/$postIdtsx
// 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>
}Navigation
导航
- Convert all components
<Link>
React Router:
tsx
import { Link } from 'react-router'
;<Link to={`/posts/${postId}`}>View Post</Link>TanStack Router:
tsx
import { Link } from '@tanstack/react-router'
;<Link to="/posts/$postId" params={{ postId }}>
View Post
</Link>Key differences:
-
is a route path pattern, NOT an interpolated string
to -
is a separate prop with typed values
params -
Active styling: useor
activeProps={{ className: 'font-bold' }}attribute for CSSdata-status="active" -
Convert allcalls
useNavigate
React Router:
tsx
import { useNavigate } from 'react-router'
const navigate = useNavigate()
navigate(`/posts/${postId}`)TanStack Router:
tsx
import { useNavigate } from '@tanstack/react-router'
const navigate = useNavigate()
navigate({ to: '/posts/$postId', params: { postId } })- 转换所有 组件
<Link>
React Router:
tsx
import { Link } from 'react-router'
;<Link to={`/posts/${postId}`}>View Post</Link>TanStack Router:
tsx
import { Link } from '@tanstack/react-router'
;<Link to="/posts/$postId" params={{ postId }}>
View Post
</Link>关键差异:
-
是路由路径模式,而非插值字符串
to -
是单独的属性,带有类型化的值
params -
激活样式:使用或
activeProps={{ className: 'font-bold' }}属性用于 CSSdata-status="active" -
转换所有调用
useNavigate
React Router:
tsx
import { useNavigate } from 'react-router'
const navigate = useNavigate()
navigate(`/posts/${postId}`)TanStack Router:
tsx
import { useNavigate } from '@tanstack/react-router'
const navigate = useNavigate()
navigate({ to: '/posts/$postId', params: { postId } })Search Params
搜索参数
- Replace with
useSearchParams+validateSearchuseSearch
React Router:
tsx
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:
tsx
// 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 }) })
}
}Key differences:
- Search params are validated and typed at the route level
- returns typed objects, not
useSearch()URLSearchParams - Update with function form to preserve other params
- 用 +
validateSearch替换useSearchuseSearchParams
React Router:
tsx
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:
tsx
// 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 - 使用函数形式更新以保留其他参数
Path Params
路径参数
- Update with
useParamspropertyfrom
React Router:
tsx
import { useParams } from 'react-router'
const { postId } = useParams()TanStack Router:
tsx
import { useParams } from '@tanstack/react-router'
const { postId } = useParams({ from: '/posts/$postId' })Or from within the route component:
tsx
const { postId } = Route.useParams()- 更新 并添加
useParams属性from
React Router:
tsx
import { useParams } from 'react-router'
const { postId } = useParams()TanStack Router:
tsx
import { useParams } from '@tanstack/react-router'
const { postId } = useParams({ from: '/posts/$postId' })或在路由组件内部使用:
tsx
const { postId } = Route.useParams()useLocation
— Common Pitfall
useLocationuseLocation
— 常见陷阱
useLocation- Replace with specific hooks
useLocation
React Router's is heavily used, and TanStack Router has a hook with the same name — but they are NOT equivalent. TanStack Router's returns the router's current location, which during pending navigations may differ from what's currently rendered. Most React Router usage should be replaced with more specific hooks. See #3110.
useLocationuseLocation()useLocationReplace based on what you actually need:
tsx
// 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' })- 用特定钩子替换
useLocation
React Router 的 被广泛使用,而 TanStack Router 也有同名钩子——但它们并不等价。TanStack Router 的 返回路由的当前位置,在待处理导航期间可能与当前渲染的内容不同。大多数 React Router 的 用法都应该替换为更具体的钩子。参见 #3110。
useLocationuseLocation()useLocation请根据实际需求进行替换:
tsx
// React Router
import { useLocation } from 'react-router'
const location = useLocation()
// ❌ 不要直接换成 TanStack Router 的 useLocation —— 它返回的是「实时」URL
import { useLocation } from '@tanstack/react-router'
// ✅ 请根据实际需求使用对应的特定钩子:
import {
useMatch,
useMatches,
useParams,
useSearch,
} from '@tanstack/react-router'
// 当前路由匹配(替代大多数 useLocation().pathname 的用法)
const match = useMatch({ from: '/posts/$postId' })
// 所有激活的匹配项(替代面包屑/数据分析场景的 useLocation)
const matches = useMatches()
// 路径参数(替代 useLocation + 手动解析)
const { postId } = useParams({ from: '/posts/$postId' })
// 搜索参数(替代 useLocation().search 解析)
const { page } = useSearch({ from: '/posts' })Outlet
Outlet
- Replace React Router with TanStack Router
OutletOutlet
The API is identical — just change the import:
tsx
// Before
import { Outlet } from 'react-router'
// After
import { Outlet } from '@tanstack/react-router'- 用 TanStack Router 的 替换 React Router 的
OutletOutlet
API 完全相同——只需修改导入:
tsx
// 之前
import { Outlet } from 'react-router'
// 之后
import { Outlet } from '@tanstack/react-router'Loaders
Loaders
- Convert React Router loaders
React Router (v7):
tsx
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:
tsx
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>
}Key differences:
- Loader is a route option, not a separate export
- is called via
useLoaderData()(orRoute.useLoaderData())useLoaderData({ from }) - TanStack Router loaders run on the CLIENT by default (not server-only)
- No wrapper needed — return plain objects
json()
- 转换 React Router 的 loader
React Router (v7):
tsx
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:
tsx
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>
}关键差异:
- Loader 是路由配置项,而非单独的导出
- 通过
useLoaderData()调用(或Route.useLoaderData())useLoaderData({ from }) - TanStack Router 的 loader 默认在客户端运行(而非仅服务端)
- 不需要 包装,直接返回普通对象
json()
Code Splitting
代码分割
- Convert lazy route imports — with in the plugin config, this is automatic. For manual splitting, use
autoCodeSplitting: truefiles:.lazy.tsx
tsx
// src/routes/lazy-page.lazy.tsx
import { createLazyFileRoute } from '@tanstack/react-router'
export const Route = createLazyFileRoute('/lazy-page')({
component: () => <div>Lazy loaded</div>,
})- 转换懒路由导入——如果插件配置中开启了 ,这一步是自动完成的。手动分割请使用
autoCodeSplitting: true文件:.lazy.tsx
tsx
// src/routes/lazy-page.lazy.tsx
import { createLazyFileRoute } from '@tanstack/react-router'
export const Route = createLazyFileRoute('/lazy-page')({
component: () => <div>Lazy loaded</div>,
})Cleanup
清理工作
- Remove React Router and verify
bash
npm uninstall react-router react-router-dom
grep -r "from 'react-router" src/ # find stale imports
npx tsc --noEmit # verify clean build- Test all routes — verify rendering, navigation (incl. back/forward), search params, dynamic params, and loaders
- 移除 React Router 并验证
bash
npm uninstall react-router react-router-dom
grep -r "from 'react-router" src/ # 查找残留导入
npx tsc --noEmit # 验证构建是否正常- 测试所有路由——验证渲染、导航(包括前进/后退)、搜索参数、动态参数和 loader 是否正常工作
Common Mistakes
常见错误
1. HIGH: Leaving React Router imports alongside TanStack Router
1. 高优先级:同时保留 React Router 与 TanStack Router 的导入
Both libraries export , , , etc. Leftover React Router imports cause "cannot use useNavigate outside of context" errors because the wrong context provider is used.
LinkuseNavigateOutlettsx
// 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'Fix: Uninstall / completely. TypeScript will flag every stale import.
react-routerreact-router-dom两个库都导出 、、 等组件和钩子。残留的 React Router 导入会导致「cannot use useNavigate outside of context」错误,因为使用了错误的上下文提供者。
LinkuseNavigateOutlettsx
// 错误 —— 混合导入
import { Link } from '@tanstack/react-router'
import { useNavigate } from 'react-router' // <- 仍然是 React Router!
// 正确 —— 全部来自 TanStack Router
import { Link, useNavigate } from '@tanstack/react-router'修复方法:完全卸载 /,TypeScript 会标记出所有残留的导入。
react-routerreact-router-dom2. HIGH: Using React Router useSearchParams
pattern
useSearchParams2. 高优先级:使用 React Router 的 useSearchParams
模式
useSearchParamstsx
// 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 neededtsx
// 错误 —— React Router 模式,返回 URLSearchParams
const [searchParams, setSearchParams] = useSearchParams()
const page = Number(searchParams.get('page'))
// 正确 —— TanStack Router 模式,返回类型化对象
// 路由定义中:
validateSearch:
z.object({
page: z.number().default(1).catch(1),
}),
// 组件中:
const { page } = Route.useSearch()
// page 已经是 number 类型 —— 不需要手动转换3. HIGH: Interpolating params into to
string
to3. 高优先级:将 params 插值到 to
字符串中
totsx
// 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>tsx
// 错误 —— React Router 的使用习惯
<Link to={`/posts/${postId}`}>Post</Link>
// 正确 —— TanStack Router:路径模式 + params 属性
<Link to="/posts/$postId" params={{ postId }}>Post</Link>4. MEDIUM: Using :param
syntax instead of $param
:param$param4. 中优先级:使用 :param
语法而非 $param
:param$paramtext
React Router: /posts/:postId
TanStack Router: /posts/$postIdFile naming also uses :
$src/routes/posts/$postId.tsxtext
React Router: /posts/:postId
TanStack Router: /posts/$postId文件命名也使用 :
$src/routes/posts/$postId.tsxQuick Reference: API Mapping
快速参考:API 映射表
| React Router v7 | TanStack Router |
|---|---|
| |
| File-based: |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| Use mutations / form libraries |
| |
| |
| |
| React Router v7 | TanStack Router |
|---|---|
| |
| 文件式路由: |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| 使用 mutations / 表单库 |
| |
| |
| |