mcpField
M
migrate-from-react-router

migrate-from-react-router

github.com/tanstack开发15.3k181 次安装4 天前更新
代码仓库

关于 migrate-from-react-router

这是一份将应用从 React Router v7 迁移到 TanStack Router 的分步清单,面向负责路由改造的开发者,涵盖路由、导航、数据加载及迁移易错点。

主要功能

  1. 01按顺序排列的迁移清单
  2. 02文件路由与动态路由转换
  3. 03导航及参数更新
  4. 04搜索参数与加载器迁移
  5. 05清理与路由测试指引

适用场景

  1. 01规划 React Router v7 迁移
  2. 02转换路由定义和导航
  3. 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 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 })使用变更操作/表单库
lazy(() => import(...))autoCodeSplitting 或 .lazy.tsx 文件
:paramName$paramName
*(通配)$(通配,通过 _splat 访问)

评论

支持 Markdown