2024 年底 React Router v7 正式发布,标志着前端路由历史上最大规模的框架合并——Remix 团队将其全部框架能力并入 React Router,开发者不再需要在「轻量路由库」和「全栈框架」之间做选择。截至 2026 年中,React Router v7 已经成为 React 生态中事实标准的路由方案,每周下载量突破 2500 万次。
这次合并的意义远超版本号的递增。Remix 从诞生之日起就提出了「Web 标准优先」的理念——拥抱原生 <Form> 表单、利用 HTTP 语义、通过 Loader/Action 分离读写操作。但 Remix 作为独立框架,始终面临和 Next.js 的正面竞争压力。将 Remix 的全栈能力融入 React Router 这个「基础设施级」包,意味着所有 React 项目——无论使用 Vite、Webpack 还是其他构建工具——都能以最低成本获得全栈开发能力。
🔀 一、从「路由库」到「全栈框架」:理解 v7 的架构变革
React Router v7 最核心的变化是引入了双模式架构——同一个包同时支持「库模式」和「框架模式」。这不是简单的功能叠加,而是对整个 React 应用构建方式的重新定义。
1.1 库模式 vs 框架模式
**库模式(Library Mode)**延续了 v6 的使用方式,适合已有项目平滑升级。你继续用 <BrowserRouter> 和 <Routes> 声明路由,与 Vite、Next.js 等其他构建工具配合使用。
**框架模式(Framework Mode)**则是 Remix 能力的完整移植。启用后,React Router 接管了整个应用的构建流程,提供文件系统路由、服务端渲染(SSR)、数据加载(Loader)、数据变更(Action)、Server Actions 等全栈能力。
// 库模式:继续使用传统方式
// app.tsx
import { BrowserRouter, Routes, Route } from 'react-router-dom'
function App() {
return (
<BrowserRouter>
<Routes>
<Route path="/" element={<Home />} />
<Route path="/about" element={<About />} />
</Routes>
</BrowserRouter>
)
}
// 框架模式:使用文件系统路由
// app/routes/home.tsx
export function loader() {
return { title: 'Hello React Router v7' }
}
export default function Home({ loaderData }) {
return <h1>{loaderData.title}</h1>
}
1.2 配置文件 react-router.config.ts
框架模式的核心配置文件是项目根目录下的 react-router.config.ts:
// react-router.config.ts
import type { Config } from '@react-router/dev/config'
export default {
// 启用 SSR
ssr: true,
// 自定义路由目录(默认 app/routes)
routes: async (defineRoutes) => {
return defineRoutes((route) => {
route('/', 'pages/home.tsx', { id: 'home' })
route('/blog/:slug', 'pages/blog.tsx', { id: 'blog-post' })
})
},
} satisfies Config
💡 提示:
ssr: false可以关闭服务端渲染,此时 React Router 退化为纯客户端 SPA 框架,但仍然保留文件系统路由和数据加载能力。
1.3 与 Next.js 的关键区别
很多开发者会问:既然 React Router 也支持 SSR,为什么不用 Next.js?核心区别在于渲染策略和数据流模型:
| 特性 | React Router v7 | Next.js (App Router) |
|---|---|---|
| SSR 策略 | 流式 SSR + 选择性 hydration | RSC 优先 + 流式 SSR |
| 数据加载 | Loader 函数(服务端) | Server Components 直接查询 |
| 数据变更 | Action 函数 + Server Actions | Server Actions |
| 路由定义 | 文件系统 + 配置混合 | 约定式文件系统 |
| 嵌套路由 | 原生支持,独立数据加载 | 部分支持,Layout 限制 |
| 构建工具 | Vite | Turbopack / Webpack |
| 中间件 | 无内置,通过 Loader 模拟 | 内置 Middleware |
| 缓存策略 | 开发者完全控制 | ISR / PPR 自动缓存 |
⚠️ 警告: 如果你的项目重度依赖 ISR(增量静态再生成)或 React Server Components 的组件级服务端逻辑,Next.js 仍然是更好的选择。React Router v7 的优势在于路由层的灵活性和嵌套路由的数据并行加载。
🚀 二、框架模式核心能力实战
框架模式是 v7 最大的亮点。下面通过一个完整的博客系统示例,展示核心 API 的实际用法。
2.1 路由类型与文件约定
React Router v7 支持四种路由类型:
app/
├── routes/
│ ├── home.tsx # 路径路由 → /
│ ├── about.tsx # 路径路由 → /about
│ ├── blog.$slug.tsx # 动态路由 → /blog/:slug
│ ├── blog._index.tsx # 索引路由 → /blog
│ ├── blog.tsx # 布局路由 → /blog/*
│ ├── dashboard.settings.tsx # 点号分隔 → /dashboard/settings
│ └── _auth.tsx # 无路径布局(_ 前缀)
文件命名规则:
- **点号(.)**表示 URL 路径分隔符
$表示动态参数_index表示索引路由_前缀表示无路径布局路由(类似 Remix 的layout)
2.2 Loader:服务端数据加载
Loader 在服务端执行,为页面组件提供初始数据。它是 React Router v7 数据流的核心:
// app/routes/blog.$slug.tsx
import type { Route } from './+types/blog.$slug'
import { getPostBySlug } from '~/models/post.server'
export async function loader({ params, request }: Route.LoaderArgs) {
const post = await getPostBySlug(params.slug)
if (!post) {
throw new Response('Not Found', { status: 404 })
}
// 获取请求头信息
const userAgent = request.headers.get('user-agent')
return {
post,
meta: {
renderedAt: new Date().toISOString(),
isBot: /bot|crawler/i.test(userAgent || ''),
},
}
}
export default function BlogPost({ loaderData }: Route.ComponentProps) {
const { post, meta } = loaderData
return (
<article>
<h1>{post.title}</h1>
<time>{new Date(post.publishedAt).toLocaleDateString('zh-CN')}</time>
<div dangerouslySetInnerHTML={{ __html: post.content }} />
{meta.isBot && <p className="sr-only">搜索引擎友好版本</p>}
</article>
)
}
📌 记住: Loader 函数只在服务端运行,永远不要在 Loader 中使用浏览器 API(如
window、document)。Loader 的返回值会自动序列化为 JSON 传递给客户端组件。
2.3 Action:数据变更与表单处理
Action 负责处理 POST、PUT、DELETE 等写操作。配合 HTML <Form> 组件,可以实现无需 JavaScript 的表单提交:
// app/routes/contact.tsx
import type { Route } from './+types/contact'
import { redirect } from 'react-router'
import { sendEmail } from '~/services/email.server'
export async function action({ request }: Route.ActionArgs) {
const formData = await request.formData()
const email = formData.get('email') as string
const message = formData.get('message') as string
// 服务端验证
const errors: Record<string, string> = {}
if (!email?.includes('@')) {
errors.email = '请输入有效的邮箱地址'
}
if (!message || message.length < 10) {
errors.message = '消息内容至少 10 个字符'
}
if (Object.keys(errors).length > 0) {
return { errors, values: { email, message } }
}
await sendEmail({ to: 'admin@jsjson.com', from: email, body: message })
return redirect('/contact/success')
}
export default function Contact({ actionData }: Route.ComponentProps) {
const errors = actionData?.errors
return (
<Form method="post">
<div>
<label htmlFor="email">邮箱</label>
<input id="email" name="email" type="email" required />
{errors?.email && <span className="error">{errors.email}</span>}
</div>
<div>
<label htmlFor="message">消息</label>
<textarea id="message" name="message" required />
{errors?.message && <span className="error">{errors.message}</span>}
</div>
<button type="submit">发送</button>
</Form>
)
}
⚠️ 警告: Action 函数中的
request.formData()只能调用一次。如果你需要多次访问表单数据,先将其存入变量:const formData = await request.formData()。
2.4 嵌套路由与并行数据加载
嵌套路由是 React Router v7 最强大的特性之一。父路由和子路由的 Loader 会并行执行,避免了串行请求的瀑布流问题:
// app/routes/dashboard.tsx —— 父布局
export async function loader() {
const user = await getCurrentUser()
const notifications = await getNotifications(user.id)
return { user, notifications }
}
export default function Dashboard({ loaderData, Outlet }) {
const { user, notifications } = loaderData
return (
<div className="dashboard">
<Sidebar user={user} notifications={notifications} />
<main>
{/* 子路由在此渲染 */}
<Outlet />
</main>
</div>
)
}
// app/routes/dashboard.overview.tsx —— 子路由
export async function loader() {
const stats = await getDashboardStats()
return { stats }
}
export default function Overview({ loaderData }) {
return <StatsPanel data={loaderData.stats} />
}
当用户访问 /dashboard/overview 时,dashboard.tsx 和 dashboard.overview.tsx 的 Loader 会同时发起请求,总耗时等于两者中最慢的那个,而非两者之和。
🔧 三、从 v6 和 Remix 迁移实战
3.1 从 React Router v6 迁移到 v7(库模式)
对于已有 v6 项目,最安全的迁移路径是先以库模式升级,再逐步启用框架模式:
# 安装 v7
npm install react-router@^7 @types/react-router-dom@^7
# 如果只需要库模式
npm install react-router-dom@^7
关键 API 变更:
// ❌ v6 写法
import { BrowserRouter, Routes, Route, useLoaderData } from 'react-router-dom'
// ✅ v7 库模式写法
import {
BrowserRouter,
Routes,
Route,
useLoaderData,
// 新增类型导出
type LoaderFunctionArgs,
type ActionFunctionArgs,
} from 'react-router'
// ❌ v6 的 Outlet
import { Outlet } from 'react-router-dom'
// ✅ v7 统一从 react-router 导入
import { Outlet } from 'react-router'
💡 提示: v7 将
react-router-dom的所有功能合并到了react-router包中。react-router-dom仍然存在,但只是react-router的简单重导出。新项目建议直接使用react-router。
3.2 从 Remix v2 迁移到 React Router v7
Remix v2 到 React Router v7 的迁移更加平滑,因为 v7 本身就是 Remix 的延续:
# 1. 替换依赖
npm uninstall @remix-run/react @remix-run/node @remix-run/dev
npm install react-router @react-router/node @react-router/dev @react-router/serve
# 2. 更新 vite.config.ts
import { reactRouter } from '@react-router/dev/vite'
// 替代原来的 remix vite 插件
# 3. 更新 tsconfig.json
{
"compilerOptions": {
"paths": {
"~/*": ["./app/*"]
},
"rootDirs": [".", "./.react-router/types"]
}
}
# 4. 运行类型生成
npx react-router typegen
关键文件变更对照表:
| Remix v2 文件 | React Router v7 文件 | 变更说明 |
|---|---|---|
app/root.tsx |
app/root.tsx |
基本不变,更新 import 路径 |
app/entry.server.tsx |
app/entry.server.tsx |
从 @remix-run/node 改为 react-router |
app/entry.client.tsx |
app/entry.client.tsx |
从 @remix-run/react 改为 react-router |
remix.config.js |
react-router.config.ts |
TypeScript 配置,新增 ssr 选项 |
@remix-run/css-bundle |
移除 | v7 使用 Vite 原生 CSS 处理 |
3.3 迁移避坑指南
坑点 1:useLoaderData 类型推断失效
v7 引入了类型生成功能,但需要先运行 npx react-router typegen。如果你看到 useLoaderData 返回 unknown,说明类型文件没有生成。
// ❌ 迁移后常见错误
const data = useLoaderData() // 类型为 unknown
// ✅ 正确做法:使用生成的 Route 类型
import type { Route } from './+types/blog.$slug'
export async function loader({ params }: Route.LoaderArgs) {
return { title: 'Hello' }
}
export default function Page({ loaderData }: Route.ComponentProps) {
// loaderData 自动获得 { title: string } 类型
return <h1>{loaderData.title}</h1>
}
坑点 2:嵌套路由的数据依赖
Remix 中子路由可以直接 import 父路由的类型。v7 中需要通过 Route.ComponentProps 的 matches 属性获取父路由数据:
// ✅ v7 获取父路由数据
import type { Route } from './+types/dashboard.overview'
export default function Overview({ matches }: Route.ComponentProps) {
// matches 是从根到当前路由的所有匹配结果数组
const dashboardData = matches.find(
(m) => m.id === 'routes/dashboard'
)?.data
return <div>{dashboardData?.user.name}</div>
}
坑点 3:json() 和 redirect() 的导入路径变更
// ❌ Remix v2
import { json, redirect } from '@remix-run/node'
// ✅ React Router v7
import { json, redirect } from 'react-router'
// 或直接返回 Response 对象
export async function loader() {
return Response.json({ data: 'hello' })
// 等价于 return json({ data: 'hello' })
}
💡 四、性能优化与生产实践
4.1 利用 clientLoader 实现客户端缓存
v7 支持 clientLoader,它只在客户端执行,可以用来实现数据缓存和乐观更新:
// app/routes/blog.$slug.tsx
import type { Route } from './+types/blog.$slug'
// 服务端 Loader:首次加载和 SEO 时执行
export async function loader({ params }: Route.LoaderArgs) {
const post = await getPostBySlug(params.slug)
return { post, cachedAt: Date.now() }
}
// 客户端 Loader:客户端导航时优先执行
export async function clientLoader({
serverLoader,
params,
}: Route.ClientLoaderArgs) {
// 检查客户端缓存
const cached = sessionStorage.getItem(`post-${params.slug}`)
if (cached) {
return JSON.parse(cached)
}
// 缓存未命中,调用服务端 Loader
const data = await serverLoader()
sessionStorage.setItem(`post-${params.slug}`, JSON.stringify(data))
return data
}
// 标记此路由在 SSR 时也执行 clientLoader
clientLoader.hydrate = true
4.2 使用 shouldRevalidate 控制重新验证
当用户在嵌套路由间切换时,默认行为是重新验证所有匹配路由的 Loader。对于不常变化的数据,可以通过 shouldRevalidate 跳过不必要的请求:
export function shouldRevalidate({
currentUrl,
nextUrl,
defaultShouldRevalidate,
}: Route.ShouldRevalidateArgs) {
// 只在路径变化时重新验证,搜索参数变化时跳过
if (currentUrl.pathname === nextUrl.pathname) {
return false
}
return defaultShouldRevalidate
}
4.3 预获取与 Link 组件
React Router v7 的 <Link> 组件支持自动预获取,大幅减少页面切换的感知延迟:
import { Link } from 'react-router'
// 默认行为:hover 时预获取 Loader 数据
<Link to="/blog/my-post">阅读全文</Link>
// 显式预获取(移动端 touchstart 触发)
<Link to="/blog/my-post" prefetch="intent">阅读全文</Link>
// 渲染时立即预获取(适合首屏关键链接)
<Link to="/blog/popular-post" prefetch="render">热门文章</Link>
// 禁用预获取
<Link to="/settings" prefetch="none">设置</Link>
4.4 useNavigation:表单提交状态管理
在传统的 React 表单中,开发者需要手动管理 loading 状态。React Router v7 提供了 useNavigation Hook,自动追踪路由转换和表单提交的状态:
// app/routes/login.tsx
import { Form, useNavigation } from 'react-router'
export default function LoginPage() {
const navigation = useNavigation()
// navigation.state 有三种状态:idle | submitting | loading
const isSubmitting = navigation.state === 'submitting'
const isLoading = navigation.state === 'loading'
return (
<Form method="post">
<input name="email" type="email" required />
<input name="password" type="password" required />
<button type="submit" disabled={isSubmitting}>
{isSubmitting ? '登录中...' : '登录'}
</button>
{/* 全局加载指示器 */}
{isLoading && (
<div className="fixed top-0 left-0 w-full h-1 bg-blue-500 animate-pulse" />
)}
</Form>
)
}
💡 提示:
useNavigation只追踪由<Form>组件或useSubmit触发的导航。普通的<Link>点击和useNavigate()编程式导航同样会被追踪。
4.5 useFetcher:后台数据操作
有时候你需要在不触发页面导航的情况下执行数据操作——比如「收藏文章」「点赞」「加载更多评论」等。useFetcher 就是为此设计的:
// app/components/like-button.tsx
import { useFetcher } from 'react-router'
export function LikeButton({ postId, initialLikes }: {
postId: string
initialLikes: number
}) {
const fetcher = useFetcher()
// 乐观更新:在服务端响应前就更新 UI
const likes = fetcher.formData
? Number(fetcher.formData.get('likes')) + 1
: initialLikes
const isLiking = fetcher.state !== 'idle'
return (
<fetcher.Form method="post" action={`/api/posts/${postId}/like`}>
<input type="hidden" name="likes" value={initialLikes} />
<button
type="submit"
disabled={isLiking}
className="flex items-center gap-1"
>
<span className={isLiking ? 'animate-bounce' : ''}>❤️</span>
<span>{likes}</span>
</button>
</fetcher.Form>
)
}
useFetcher 和 <Form> 的关键区别在于:fetcher 不会触发路由导航,不会改变 URL,也不会触发嵌套路由的 Loader 重新执行。它适合「局部更新」场景。
4.6 错误处理:ErrorBoundary 与边界捕获
React Router v7 的错误处理模型继承自 Remix,采用路由级错误边界:
// app/routes/dashboard.tsx
import {
isRouteErrorResponse,
useRouteError,
Outlet,
} from 'react-router'
// 正常组件
export default function Dashboard() {
return (
<div>
<h1>Dashboard</h1>
<Outlet />
</div>
)
}
// 错误边界组件:捕获子路由和 Loader/Action 中的错误
export function ErrorBoundary() {
const error = useRouteError()
// HTTP 响应错误(如 throw new Response)
if (isRouteErrorResponse(error)) {
return (
<div className="error-page">
<h1>{error.status} {error.statusText}</h1>
<p>{error.data}</p>
</div>
)
}
// JavaScript 运行时错误
return (
<div className="error-page">
<h1>出错了</h1>
<p>{error instanceof Error ? error.message : '未知错误'}</p>
</div>
)
}
⚠️ 警告:
ErrorBoundary只能捕获其直接子路由的错误。如果子路由有自己的ErrorBoundary,错误会被子路由的边界捕获,不会向上传播。合理规划错误边界的层级非常重要。
✅ 总结与建议
React Router v7 的发布代表了 React 生态路由层的统一。对于不同类型项目,我的建议是:
- ✅ 已有 React Router v6 项目:先以库模式升级到 v7,享受类型改进和新 API,不需要大改架构
- ✅ 已有 Remix v2 项目:直接迁移,几乎零成本,享受持续的维护和新特性
- ✅ 新项目需要全栈能力:考虑框架模式,特别适合需要嵌套路由并行加载的中大型应用
- ❌ 已深度使用 Next.js App Router:不建议迁移,两者模型差异较大,迁移成本高
- ⚠️ 需要 ISR / 静态生成:React Router v7 的静态生成能力仍在发展中,Next.js 仍然是更好的选择
⚡ 关键结论: React Router v7 不是要取代 Next.js,而是提供了一种不同的全栈构建范式。选择哪个框架,取决于你的团队更偏好「路由驱动」还是「组件驱动」的数据流模型。
推荐相关工具:
- 🔧 React Router 官方文档 — 完整的 API 参考
- 🔧 Vite — React Router v7 框架模式的底层构建工具
- 🔧 jsjson.com JSON 格式化工具 — 格式化 API 响应中的 JSON 数据
- 🔧 jsjson.com JWT 解码工具 — 调试认证相关的 JWT Token