migrate-from-react-router

Compare original and translation side by side

🇺🇸

Original

English
🇨🇳

Translation

Chinese

Migrate 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
react-router
(and
react-router-dom
if present) to surface them as TypeScript errors.
CRITICAL: TanStack Router uses
to
+
params
for navigation, NOT template literal paths. Never interpolate params into the
to
string.
NOTE: React Router v7 recommends importing from
react-router
(not
react-router-dom
). The
react-router-dom
package still exists but just re-exports from
react-router
. Check for imports from both.
这是一份分步迁移检查清单,每一项对应一个转换任务,请按顺序完成。
关键提示:如果迁移后 UI 显示空白,请打开控制台。类似「cannot use useNavigate outside of context」的错误说明仍存在 React Router 与 TanStack Router 的导入并存的情况。卸载
react-router
(如果有
react-router-dom
也一并卸载),这样这些残留导入会以 TypeScript 错误的形式暴露出来。
关键提示: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/routes

Route Definitions

路由定义

  • Create root route
React Router:
<BrowserRouter>
or
createBrowserRouter([{ element: <Layout />, children: [...] }])
TanStack Router:
src/routes/__root.tsx
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 />
    </>
  ),
})
  • 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:
/posts/:postId
(colon syntax)
TanStack Router:
/posts/$postId
(dollar syntax)
tsx
// 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.tsx
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 />
    </>
  ),
})
  • 创建带类型注册的路由实例
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/:postId
(冒号语法)
TanStack Router:
/posts/$postId
(美元符号语法)
tsx
// 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
    <Link>
    components
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:
  • to
    is a route path pattern, NOT an interpolated string
  • params
    is a separate prop with typed values
  • Active styling: use
    activeProps={{ className: 'font-bold' }}
    or
    data-status="active"
    attribute for CSS
  • Convert all
    useNavigate
    calls
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' }}
    或
    data-status="active"
    属性用于 CSS
  • 转换所有
    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
    useSearchParams
    with
    validateSearch
    +
    useSearch
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
  • useSearch()
    returns typed objects, not
    URLSearchParams
  • Update with function form to preserve other params
  • 用
    validateSearch
    +
    useSearch
    替换
    useSearchParams
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
    useParams
    with
    from
    property
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

useLocation
— 常见陷阱

  • Replace
    useLocation
    with specific hooks
React Router's
useLocation
is heavily used, and TanStack Router has a hook with the same name — but they are NOT equivalent. TanStack Router's
useLocation()
returns the router's current location, which during pending navigations may differ from what's currently rendered. Most React Router
useLocation
usage should be replaced with more specific hooks. See #3110.
Replace 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 的
useLocation
被广泛使用,而 TanStack Router 也有同名钩子——但它们并不等价。TanStack Router 的
useLocation()
返回路由的当前位置,在待处理导航期间可能与当前渲染的内容不同。大多数 React Router 的
useLocation
用法都应该替换为更具体的钩子。参见 #3110。
请根据实际需求进行替换:
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
    Outlet
    with TanStack Router
    Outlet
The API is identical — just change the import:
tsx
// Before
import { Outlet } from 'react-router'

// After
import { Outlet } from '@tanstack/react-router'
  • 用 TanStack Router 的
    Outlet
    替换 React Router 的
    Outlet
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
  • useLoaderData()
    is called via
    Route.useLoaderData()
    (or
    useLoaderData({ from })
    )
  • TanStack Router loaders run on the CLIENT by default (not server-only)
  • No
    json()
    wrapper needed — return plain objects
  • 转换 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
    autoCodeSplitting: true
    in the plugin config, this is automatic. For manual splitting, use
    .lazy.tsx
    files:
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
Link
,
useNavigate
,
Outlet
, etc. Leftover React Router imports cause "cannot use useNavigate outside of context" errors because the wrong context provider is used.
tsx
// 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
react-router
/
react-router-dom
completely. TypeScript will flag every stale import.
两个库都导出
Link
、
useNavigate
、
Outlet
等组件和钩子。残留的 React Router 导入会导致「cannot use useNavigate outside of context」错误,因为使用了错误的上下文提供者。
tsx
// 错误 —— 混合导入
import { Link } from '@tanstack/react-router'
import { useNavigate } from 'react-router' // <- 仍然是 React Router!

// 正确 —— 全部来自 TanStack Router
import { Link, useNavigate } from '@tanstack/react-router'
修复方法:完全卸载
react-router
/
react-router-dom
,TypeScript 会标记出所有残留的导入。

2. HIGH: Using React Router
useSearchParams
pattern

2. 高优先级:使用 React Router 的
useSearchParams
模式

tsx
// 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
tsx
// 错误 —— 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

3. 高优先级:将 params 插值到
to
字符串中

tsx
// 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

4. 中优先级:使用
:param
语法而非
$param

text
React Router: /posts/:postId
TanStack Router: /posts/$postId
File naming also uses
$
:
src/routes/posts/$postId.tsx
text
React Router: /posts/:postId
TanStack Router: /posts/$postId
文件命名也使用
$
:
src/routes/posts/$postId.tsx

Quick Reference: API Mapping

快速参考:API 映射表

React Router v7TanStack Router
<BrowserRouter>
<RouterProvider router={router} />
<Routes>
/
<Route>
File-based:
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 }) => ...
(route option)
action({ request })
Use mutations / form libraries
lazy(() => import(...))
autoCodeSplitting
or
.lazy.tsx
files
:paramName
$paramName
*
(splat)
$
(splat, accessed via
_splat
)
React Router v7TanStack 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 })
使用 mutations / 表单库
lazy(() => import(...))
autoCodeSplitting
或
.lazy.tsx
文件
:paramName
$paramName
*
(splat)
$
(splat, 通过
_splat
访问)