「React Router v7 教程」从零到全栈,一篇搞定
一篇文掌握 React Router v7 的全部核心知识,从基础路由到全栈框架
开篇:为什么需要路由
如果你用过 React,大概率遇到过这个问题:你的应用只有一个页面。
不管你点什么、做什么,URL 都是 http://localhost:3000/,不会变。这会带来几个实际问题:
- 用户想分享某个页面,但 URL 永远是同一个
- 用户想用浏览器的后退按钮,但点了没反应
- 搜索引擎不知道你的应用有哪些页面
这些问题的根源是:单页应用(SPA)只有一个 HTML 文件,所有内容都是 JavaScript 动态切换的。
React Router 就是为了解决这个问题。它让 URL 和 UI 保持同步------URL 变了,UI 就跟着变;UI 变了,URL 也跟着变。
本文会覆盖什么
- 基础路由配置
- 嵌套路由与布局复用
- 动态路由与参数处理
- 编程式导航与高级功能
- Framework Mode 全栈能力
- 两种模式的选择指南
Part 1: 快速上手:5 分钟搞定路由配置
包名变化
React Router v7 做了一个重要改变:统一了包名。
| 版本 | 包名 | 导入方式 |
|---|---|---|
| v6 | react-router-dom |
import { BrowserRouter } from "react-router-dom" |
| v7 | react-router |
import { BrowserRouter } from "react-router" |
个人建议:如果你是从 v6 迁移过来的,第一步就是把包名改了。其他代码基本不用动。
BrowserRouter vs HashRouter
React Router 提供了两种路由器:
| BrowserRouter | HashRouter | |
|---|---|---|
| URL 格式 | /about |
/#/about |
| 原理 | 使用 History API | 使用 URL hash |
| 服务器配置 | 需要配置 | 不需要 |
| 适合场景 | 有服务器控制权 | 静态托管(如 GitHub Pages) |
BrowserRouter 是更常见的选择,URL 更干净。但它需要服务器配合------当用户直接访问 /about 时,服务器需要返回 index.html,而不是 404。
HashRouter 不需要服务器配合,因为 hash 部分不会发送给服务器。但 URL 里会有一个 #,看起来不太美观。
个人建议:如果你有服务器控制权,用 BrowserRouter;如果是纯静态托管(比如 GitHub Pages),用 HashRouter。
Route 和 Link 组件
Route 是 React Router 的核心,它声明了"什么 URL 显示什么组件":
jsx
<Route path="/about" element={<About />} />
<Route path="/contact" element={<Contact />} />
Link 是 React Router 提供的导航组件,替代 HTML 的 <a> 标签:
jsx
// ❌ 用 a 标签:会导致整页刷新
<a href="/about">关于</a>
// ✅ 用 Link:只切换组件,不刷新页面
<Link to="/about">关于</Link>
为什么不能用 <a> 标签?因为 <a> 会让浏览器重新加载整个页面,失去单页应用的流畅体验。Link 组件会阻止默认行为,只切换组件。
Routes 容器
Routes 是路由容器,它会根据当前 URL 找到匹配的 Route:
jsx
<Routes>
<Route path="/" element={<Home />} />
<Route path="/about" element={<About />} />
<Route path="/contact" element={<Contact />} />
</Routes>
React Router 会按顺序匹配路由,找到第一个匹配的 Route,渲染对应的组件。
Part 2: 嵌套路由:别再重复写布局了
为什么需要嵌套路由
很多页面有"布局"------比如后台管理系统,通常有:
- 一个顶部导航栏
- 一个侧边栏
- 一个主内容区
当用户切换页面时,顶部导航栏和侧边栏不变,只有主内容区变化。
如果不用嵌套路由,你可能需要在每个页面组件里都写一遍导航栏和侧边栏:
jsx
// ❌ 每个页面都重复布局
function Dashboard() {
return (
<div>
<Header />
<Sidebar />
<main>Dashboard 内容</main>
</div>
);
}
function Settings() {
return (
<div>
<Header />
<Sidebar />
<main>设置页面</main>
</div>
);
}
这显然很蠢。嵌套路由让你只写一次布局:
jsx
// ✅ 布局只写一次
function Layout() {
return (
<div>
<Header />
<Sidebar />
<Outlet /> {/* 这里会渲染子路由 */}
</div>
);
}
Outlet 组件
Outlet 是 React Router 提供的"占位符",它会渲染匹配的子路由:
jsx
import { Outlet } from "react-router";
function Layout() {
return (
<div>
<Header />
<Sidebar />
<main>
<Outlet />
</main>
</div>
);
}
当 URL 是 /dashboard 时,Outlet 会渲染 DashboardHome 组件;当 URL 是 /dashboard/users 时,Outlet 会渲染 Users 组件。
嵌套 Route 配置
把布局和路由结合起来:
jsx
<Routes>
<Route path="/" element={<Layout />}>
<Route index element={<Home />} />
<Route path="about" element={<About />} />
<Route path="contact" element={<Contact />} />
</Route>
</Routes>
注意这里:
- 父路由
path="/"用Layout作为布局 - 子路由
index表示默认页面 - 子路由的
path不需要写/,它会自动拼接
Part 3: 动态路由:一个路由搞定所有页面
为什么需要动态路由
很多页面需要根据 URL 参数显示不同内容,比如:
/users/1显示用户 1 的信息/users/2显示用户 2 的信息/posts/123显示文章 123 的内容
如果为每个用户、每篇文章都写一个单独的路由和组件,代码会爆炸。
动态路由让你用一个路由处理所有同类页面:
jsx
<Route path="/users/:id" element={<UserDetail />} />
这里的 :id 是动态参数,它可以匹配任何值。当 URL 是 /users/1 时,id 就是 "1";当 URL 是 /users/2 时,id 就是 "2"。
useParams Hook
在组件里用 useParams 获取 URL 参数:
jsx
import { useParams } from "react-router";
function UserDetail() {
const { id } = useParams();
return <h1>用户 {id} 的详情</h1>;
}
查询参数(?key=value)
除了路径参数,URL 还可以有查询参数,比如 /search?q=react&page=2。
React Router 用 useSearchParams 处理查询参数:
jsx
import { useSearchParams } from "react-router";
function SearchPage() {
const [searchParams, setSearchParams] = useSearchParams();
const query = searchParams.get('q');
const page = searchParams.get('page') || '1';
return (
<div>
<p>搜索:{query}</p>
<p>第 {page} 页</p>
<button onClick={() => setSearchParams({ q: query, page: '2' })}>
下一页
</button>
</div>
);
}
路径参数 vs 查询参数
| 路径参数 | 查询参数 | |
|---|---|---|
| URL 格式 | /users/123 |
/users?id=123 |
| 用途 | 资源标识(必须的) | 可选的过滤、排序、分页 |
| 获取方式 | useParams() |
useSearchParams() |
| 典型场景 | 用户详情、文章详情 | 搜索、筛选、分页 |
个人建议:如果参数是资源的唯一标识(比如用户 ID),用路径参数;如果是可选的过滤条件(比如搜索关键词),用查询参数。
Part 4: 编程式导航:登录跳转、404 页面、权限守卫
为什么需要编程式导航
Link 组件适合用户主动点击的场景,但有些场景需要在代码里控制跳转:
- 用户登录成功后,跳转到首页
- 表单提交成功后,跳转到列表页
- 用户没有权限时,跳转到登录页
这些场景需要编程式导航------在 JavaScript 代码里控制 URL 变化。
useNavigate Hook
React Router 用 useNavigate Hook 实现编程式导航:
jsx
import { useNavigate } from "react-router";
function LoginPage() {
const navigate = useNavigate();
async function handleLogin(e) {
e.preventDefault();
const success = await login(username, password);
if (success) {
navigate('/'); // 跳转到首页
}
}
return (
<form onSubmit={handleLogin}>
{/* ... */}
</form>
);
}
导航时传递状态
有时候你想在跳转时传递一些数据,但不想放在 URL 里。可以用 state:
jsx
const navigate = useNavigate();
// 跳转时传递状态
navigate('/dashboard', { state: { from: 'login' } });
在目标页面读取状态:
jsx
import { useLocation } from "react-router";
function Dashboard() {
const location = useLocation();
const from = location.state?.from;
return <p>你从 {from} 页面来</p>;
}
NavLink 组件
NavLink 是 Link 的增强版,可以知道当前是否激活:
jsx
import { NavLink } from "react-router";
function Navigation() {
return (
<nav>
<NavLink
to="/"
className={({ isActive }) => isActive ? "active" : ""}
>
首页
</NavLink>
<NavLink
to="/about"
className={({ isActive }) => isActive ? "active" : ""}
>
关于
</NavLink>
</nav>
);
}
404 页面和重定向
当用户访问一个不存在的 URL 时,你需要显示 404 页面。用通配符 * 匹配所有路径:
jsx
<Routes>
<Route path="/" element={<Home />} />
<Route path="/about" element={<About />} />
<Route path="*" element={<NotFound />} /> {/* 404 */}
</Routes>
React Router 用 Navigate 组件实现重定向:
jsx
import { Navigate } from "react-router";
function OldPage() {
return <Navigate to="/new-page" replace />;
}
replace 表示替换当前历史记录,而不是添加一条新记录。这样用户后退时不会回到旧页面。
Part 5: Framework Mode:不只是路由,更是全栈框架
Framework Mode vs Library Mode
React Router v7 做了一件大事:它把 Remix 吸收进来了。这意味着 React Router 不再只是一个"客户端路由库",它现在有两种使用方式:
| Framework Mode | Library Mode | |
|---|---|---|
| 安装方式 | create-react-router |
npm install react-router |
| 路由方式 | 文件系统路由 | 组件式路由 |
| SSR | 内置支持 | 不支持 |
| 数据加载 | loader/action | 手动 fetch |
| 构建工具 | Vite(内置) | 任意 bundler |
| 适合场景 | 新项目、全栈应用 | 现有项目、纯 SPA |
个人建议:如果你是从零开始,用 Framework Mode;如果你有现有项目要迁移,先用 Library Mode。
快速开始
创建一个 Framework Mode 项目:
bash
npx create-react-router@latest my-app
cd my-app
npm install
npm run dev
项目结构:
csharp
my-app/
├── app/
│ ├── routes/
│ │ └── home.tsx # 首页路由
│ ├── root.tsx # 根布局
│ └── routes.ts # 路由配置
├── public/
├── package.json
└── vite.config.ts
文件系统路由
Framework Mode 使用文件系统定义路由。你在 app/routes/ 目录下创建文件,React Router 自动把它变成路由。
路由配置文件:
ts
// app/routes.ts
import { type RouteConfig, route, layout, index } from "@react-router/dev/routes";
export default [
index("./routes/home.tsx"),
route("about", "./routes/about.tsx"),
route("blog/:id", "./routes/blog-detail.tsx"),
] satisfies RouteConfig;
对应的文件:
bash
app/routes/
├── home.tsx → /
├── about.tsx → /about
└── blog-detail.tsx → /blog/:id
布局嵌套
很多页面共享相同的布局(比如导航栏、侧边栏)。Framework Mode 用 layout 函数实现:
ts
// app/routes.ts
import { type RouteConfig, layout, index, route } from "@react-router/dev/routes";
export default [
layout("./layouts/main.tsx", [
index("./routes/home.tsx"),
route("about", "./routes/about.tsx"),
route("blog", "./routes/blog.tsx"),
]),
] satisfies RouteConfig;
布局组件:
tsx
// app/layouts/main.tsx
import { Outlet } from "react-router";
export default function MainLayout() {
return (
<div>
<nav>
<Link to="/">首页</Link>
<Link to="/about">关于</Link>
<Link to="/blog">博客</Link>
</nav>
<main>
<Outlet /> {/* 子路由在这里渲染 */}
</main>
</div>
);
}
Outlet 是一个占位符,它会渲染当前匹配的子路由组件。
loader 数据加载
Framework Mode 最强大的功能之一是 loader。它在服务端运行,可以在组件渲染前获取数据。
tsx
// app/routes/blog.tsx
import { useLoaderData } from "react-router";
// 这个函数在服务端运行
export async function loader() {
const posts = await fetch("https://api.example.com/posts");
return posts.json();
}
export default function Blog() {
const posts = useLoaderData(); // 获取 loader 返回的数据
return (
<ul>
{posts.map(post => (
<li key={post.id}>{post.title}</li>
))}
</ul>
);
}
数据流:
scss
服务端:loader() 执行 → 获取数据 → 返回给客户端
客户端:useLoaderData() → 拿到数据 → 渲染组件
这比传统的 useEffect + fetch 更好,因为:
- 数据在服务端获取,减少客户端请求
- 数据和组件在同一个文件,代码更内聚
- 支持 SSR,首屏加载更快
action 表单处理
action 是 Framework Mode 的另一个核心功能。它在服务端运行,处理数据写入。
tsx
// app/routes/blog-new.tsx
import { redirect } from "react-router";
export async function action({ request }) {
const formData = await request.formData();
const title = formData.get("title");
const content = formData.get("content");
// 保存到数据库
await db.posts.create({ title, content });
// 重定向到文章列表
return redirect("/blog");
}
export default function NewPost() {
return (
<form method="post">
<input name="title" placeholder="标题" />
<textarea name="content" placeholder="内容" />
<button type="submit">发布</button>
</form>
);
}
数据流:
scss
用户提交表单 → action() 执行 → 处理数据 → 重定向或返回数据
useActionData:获取 action 返回的数据
有时候你想在 action 执行后获取返回的数据,比如表单验证错误:
tsx
// app/routes/login.tsx
import { useActionData } from "react-router";
export async function action({ request }) {
const formData = await request.formData();
const username = formData.get("username");
const password = formData.get("password");
// 验证
if (!username || !password) {
return { error: "用户名和密码不能为空" };
}
// 登录逻辑
const success = await login(username, password);
if (!success) {
return { error: "用户名或密码错误" };
}
return redirect("/dashboard");
}
export default function Login() {
const actionData = useActionData();
return (
<form method="post">
<input name="username" placeholder="用户名" />
<input name="password" type="password" placeholder="密码" />
{actionData?.error && <p className="error">{actionData.error}</p>}
<button type="submit">登录</button>
</form>
);
}
错误处理
Framework Mode 提供了内置的错误处理机制:
tsx
// app/routes/blog-detail.tsx
import { useRouteError, isRouteErrorResponse } from "react-router";
export async function loader({ params }) {
const post = await db.posts.findUnique({ where: { id: params.id } });
if (!post) {
throw new Response("Not Found", { status: 404 });
}
return { post };
}
export default function BlogDetail() {
const { post } = useLoaderData();
return (
<article>
<h1>{post.title}</h1>
<p>{post.content}</p>
</article>
);
}
export function ErrorBoundary() {
const error = useRouteError();
if (isRouteErrorResponse(error)) {
if (error.status === 404) {
return <h1>文章不存在</h1>;
}
return <h1>错误:{error.status}</h1>;
}
return <h1>未知错误</h1>;
}
SSR 基础
Framework Mode 内置了 SSR(服务端渲染)。这意味着:
- 首屏加载更快:HTML 在服务端生成,客户端不需要等待 JavaScript 加载
- SEO 友好:搜索引擎可以抓取完整的 HTML
- 更好的用户体验:用户立即看到内容,而不是空白页面
SSR 工作流程:
css
用户访问页面 → 服务端执行 loader → 生成 HTML → 返回给客户端
客户端加载 JavaScript → React 接管 → 页面可交互
| 传统 SPA | SSR | |
|---|---|---|
| 首屏加载 | 需要等待 JS 加载 | 立即显示 HTML |
| SEO | 不友好 | 友好 |
| 服务器压力 | 低 | 高 |
| 用户体验 | 有白屏 | 无白屏 |
个人建议:如果你的应用需要 SEO 或首屏性能,用 SSR;如果是内部系统,纯 SPA 就够了。
从 v6 角度理解 Framework Mode
如果你用过 React Router v6,Framework Mode 的概念可能有点陌生。让我用对比的方式帮你理解:
v6 的数据获取方式:
jsx
// ❌ v6:组件里写 useEffect + fetch
function BlogList() {
const [posts, setPosts] = useState([]);
const [loading, setLoading] = useState(true);
useEffect(() => {
fetch('/api/posts')
.then(res => res.json())
.then(data => {
setPosts(data);
setLoading(false);
});
}, []);
if (loading) return <p>加载中...</p>;
return (
<ul>
{posts.map(post => (
<li key={post.id}>{post.title}</li>
))}
</ul>
);
}
v7 Framework Mode 的数据获取方式:
tsx
// ✅ v7:用 loader 在服务端获取数据
import { useLoaderData } from "react-router";
export async function loader() {
const posts = await fetch('/api/posts');
return posts.json();
}
export default function BlogList() {
const posts = useLoaderData(); // 直接拿到数据,不需要 loading 状态
return (
<ul>
{posts.map(post => (
<li key={post.id}>{post.title}</li>
))}
</ul>
);
}
关键区别:
| v6 (useEffect + fetch) | v7 Framework Mode (loader) | |
|---|---|---|
| 数据获取位置 | 客户端(浏览器) | 服务端(Node.js) |
| 代码位置 | 分散在组件里 | 集中在同一个文件 |
| Loading 状态 | 需要手动管理 | 自动处理 |
| SEO | 不友好(客户端渲染) | 友好(服务端渲染) |
| 首屏性能 | 有白屏 | 无白屏 |
loader vs useEffect:什么时候用哪个?
用 loader 的场景:
- 页面首次加载就需要的数据
- 需要 SEO 的数据
- 需要首屏性能的数据
用 useEffect 的场景:
- 用户交互后才需要的数据(比如点击按钮加载更多)
- 实时数据(比如 WebSocket)
- 客户端特有的逻辑(比如读取 localStorage)
tsx
// ✅ 用 loader:文章详情(首次加载就需要)
export async function loader({ params }) {
const post = await db.posts.findUnique({ where: { id: params.id } });
return { post };
}
// ✅ 用 useEffect:点赞数(用户交互后更新)
function LikeButton({ postId }) {
const [likes, setLikes] = useState(0);
async function handleLike() {
const res = await fetch(`/api/posts/${postId}/like`, { method: 'POST' });
const data = await res.json();
setLikes(data.likes);
}
return <button onClick={handleLike}>👍 {likes}</button>;
}
action vs 传统表单处理
v6 的表单处理:
jsx
// ❌ v6:手动处理表单提交
function NewPost() {
const [title, setTitle] = useState('');
const [content, setContent] = useState('');
async function handleSubmit(e) {
e.preventDefault();
const res = await fetch('/api/posts', {
method: 'POST',
body: JSON.stringify({ title, content }),
});
if (res.ok) {
// 手动跳转
window.location.href = '/blog';
}
}
return (
<form onSubmit={handleSubmit}>
<input value={title} onChange={e => setTitle(e.target.value)} />
<textarea value={content} onChange={e => setContent(e.target.value)} />
<button type="submit">发布</button>
</form>
);
}
v7 Framework Mode 的表单处理:
tsx
// ✅ v7:用 action 处理表单
import { redirect } from "react-router";
export async function action({ request }) {
const formData = await request.formData();
const title = formData.get("title");
const content = formData.get("content");
await db.posts.create({ title, content });
return redirect("/blog");
}
export default function NewPost() {
return (
<form method="post">
<input name="title" placeholder="标题" />
<textarea name="content" placeholder="内容" />
<button type="submit">发布</button>
</form>
);
}
关键区别:
| v6 (手动处理) | v7 Framework Mode (action) | |
|---|---|---|
| 表单状态 | 需要 useState 管理 | 不需要 |
| 提交逻辑 | 写在组件里 | 写在 action 里 |
| 错误处理 | 手动处理 | 用 useActionData |
| 跳转 | 手动 window.location | 用 redirect() |
| 代码量 | 多 | 少 |
实战:从 v6 迁移到 Framework Mode
假设你有一个 v6 的博客应用,让我们看看如何迁移到 Framework Mode:
v6 的项目结构:
css
src/
├── App.tsx
├── pages/
│ ├── Home.tsx
│ ├── BlogList.tsx
│ └── BlogDetail.tsx
└── components/
└── Layout.tsx
v6 的路由配置:
tsx
// src/App.tsx
import { BrowserRouter, Routes, Route } from "react-router-dom";
import Layout from "./components/Layout";
import Home from "./pages/Home";
import BlogList from "./pages/BlogList";
import BlogDetail from "./pages/BlogDetail";
function App() {
return (
<BrowserRouter>
<Routes>
<Route element={<Layout />}>
<Route path="/" element={<Home />} />
<Route path="/blog" element={<BlogList />} />
<Route path="/blog/:id" element={<BlogDetail />} />
</Route>
</Routes>
</BrowserRouter>
);
}
迁移到 v7 Framework Mode:
arduino
app/
├── routes.ts
├── root.tsx
├── layouts/
│ └── main.tsx
└── routes/
├── home.tsx
├── blog.tsx
└── blog-detail.tsx
v7 的路由配置:
ts
// app/routes.ts
import { type RouteConfig, layout, index, route } from "@react-router/dev/routes";
export default [
layout("./layouts/main.tsx", [
index("./routes/home.tsx"),
route("blog", "./routes/blog.tsx"),
route("blog/:id", "./routes/blog-detail.tsx"),
]),
] satisfies RouteConfig;
v7 的数据获取:
tsx
// app/routes/blog.tsx
import { useLoaderData, Link } from "react-router";
// v6 的方式:useEffect + fetch
// v7 的方式:loader
export async function loader() {
const posts = await fetch('/api/posts');
return posts.json();
}
export default function BlogList() {
const posts = useLoaderData();
return (
<div>
<h1>博客列表</h1>
<ul>
{posts.map(post => (
<li key={post.id}>
<Link to={`/blog/${post.id}`}>{post.title}</Link>
</li>
))}
</ul>
</div>
);
}
Part 6: 如何选择:Framework Mode 还是 Library Mode
决策指南
| 你的情况 | 推荐模式 | 原因 |
|---|---|---|
| 新项目,想用全栈能力 | Framework Mode | 内置 SSR、数据加载、文件路由 |
| 现有 v6 项目,想升级 | Library Mode | API 兼容,迁移成本低 |
| 需要 SEO 和首屏性能 | Framework Mode | SSR 对 SEO 友好 |
| 纯客户端 SPA | Library Mode | 更轻量,不需要服务端 |
| 想学习最新技术 | Framework Mode | 代表 React 路由的未来方向 |
迁移路径一:从 v6 迁移到 Library Mode
这是最简单的迁移方式,只需要改包名,其他代码基本不用动。
第一步:更新依赖
bash
npm uninstall react-router-dom
npm install react-router
第二步:更新导入语句
tsx
// ❌ 旧的导入方式
import { BrowserRouter, Routes, Route, Link } from "react-router-dom";
// ✅ 新的导入方式
import { BrowserRouter, Routes, Route, Link } from "react-router";
第三步:检查 API 变化
大部分 API 和 v6 一样,但有一些小变化:
| v6 | v7 Library Mode | 变化 |
|---|---|---|
react-router-dom |
react-router |
包名变了 |
<Route path="/" element={<Home />} /> |
同上 | 没变 |
<Link to="/about"> |
同上 | 没变 |
useParams() |
同上 | 没变 |
useNavigate() |
同上 | 没变 |
useSearchParams() |
同上 | 没变 |
迁移检查清单:
- 更新
package.json中的依赖 - 全局替换
react-router-dom→react-router - 运行
npm install - 测试所有路由是否正常工作
- 测试所有导航是否正常工作
常见问题:
Q: 我的代码还能用吗? A: 能用。Library Mode 的 API 和 v6 几乎一样,只是包名变了。
Q: 我需要改组件代码吗? A: 不需要。只需要改导入语句。
Q: 我需要改路由配置吗? A: 不需要。<Routes>、<Route>、<Link> 的用法完全一样。
迁移路径二:从 Library Mode 升级到 Framework Mode
这是一个更大的变化,需要重新组织项目结构。但好处是能获得 SSR、数据加载等全栈能力。
第一步:创建 Framework Mode 项目
bash
npx create-react-router@latest my-app
cd my-app
npm install
第二步:迁移项目结构
v6/v7 Library Mode 的结构:
css
src/
├── App.tsx
├── pages/
│ ├── Home.tsx
│ ├── BlogList.tsx
│ └── BlogDetail.tsx
└── components/
└── Layout.tsx
Framework Mode 的结构:
arduino
app/
├── routes.ts
├── root.tsx
├── layouts/
│ └── main.tsx
└── routes/
├── home.tsx
├── blog.tsx
└── blog-detail.tsx
第三步:迁移路由配置
v6/v7 Library Mode 的路由:
tsx
// src/App.tsx
import { BrowserRouter, Routes, Route } from "react-router";
import Layout from "./components/Layout";
import Home from "./pages/Home";
import BlogList from "./pages/BlogList";
import BlogDetail from "./pages/BlogDetail";
function App() {
return (
<BrowserRouter>
<Routes>
<Route element={<Layout />}>
<Route path="/" element={<Home />} />
<Route path="/blog" element={<BlogList />} />
<Route path="/blog/:id" element={<BlogDetail />} />
</Route>
</Routes>
</BrowserRouter>
);
}
Framework Mode 的路由:
ts
// app/routes.ts
import { type RouteConfig, layout, index, route } from "@react-router/dev/routes";
export default [
layout("./layouts/main.tsx", [
index("./routes/home.tsx"),
route("blog", "./routes/blog.tsx"),
route("blog/:id", "./routes/blog-detail.tsx"),
]),
] satisfies RouteConfig;
第四步:迁移数据获取
v6/v7 Library Mode 的数据获取:
tsx
// src/pages/BlogList.tsx
import { useState, useEffect } from "react";
import { Link } from "react-router";
function BlogList() {
const [posts, setPosts] = useState([]);
const [loading, setLoading] = useState(true);
useEffect(() => {
fetch('/api/posts')
.then(res => res.json())
.then(data => {
setPosts(data);
setLoading(false);
});
}, []);
if (loading) return <p>加载中...</p>;
return (
<ul>
{posts.map(post => (
<li key={post.id}>
<Link to={`/blog/${post.id}`}>{post.title}</Link>
</li>
))}
</ul>
);
}
Framework Mode 的数据获取:
tsx
// app/routes/blog.tsx
import { useLoaderData, Link } from "react-router";
export async function loader() {
const posts = await fetch('/api/posts');
return posts.json();
}
export default function BlogList() {
const posts = useLoaderData(); // 直接拿到数据,不需要 loading 状态
return (
<ul>
{posts.map(post => (
<li key={post.id}>
<Link to={`/blog/${post.id}`}>{post.title}</Link>
</li>
))}
</ul>
);
}
第五步:迁移表单处理
v6/v7 Library Mode 的表单处理:
tsx
// src/pages/NewPost.tsx
import { useState } from "react";
import { useNavigate } from "react-router";
function NewPost() {
const navigate = useNavigate();
const [title, setTitle] = useState('');
const [content, setContent] = useState('');
const [error, setError] = useState('');
async function handleSubmit(e) {
e.preventDefault();
const res = await fetch('/api/posts', {
method: 'POST',
body: JSON.stringify({ title, content }),
});
if (res.ok) {
navigate('/blog');
} else {
setError('发布失败');
}
}
return (
<form onSubmit={handleSubmit}>
{error && <p className="error">{error}</p>}
<input value={title} onChange={e => setTitle(e.target.value)} />
<textarea value={content} onChange={e => setContent(e.target.value)} />
<button type="submit">发布</button>
</form>
);
}
Framework Mode 的表单处理:
tsx
// app/routes/blog-new.tsx
import { redirect, useActionData } from "react-router";
export async function action({ request }) {
const formData = await request.formData();
const title = formData.get("title");
const content = formData.get("content");
if (!title || !content) {
return { error: "标题和内容不能为空" };
}
await db.posts.create({ title, content });
return redirect("/blog");
}
export default function NewPost() {
const actionData = useActionData();
return (
<form method="post">
{actionData?.error && <p className="error">{actionData.error}</p>}
<input name="title" placeholder="标题" />
<textarea name="content" placeholder="内容" />
<button type="submit">发布</button>
</form>
);
}
迁移检查清单:
- 创建新的 Framework Mode 项目
- 迁移路由配置到
app/routes.ts - 迁移布局组件到
app/layouts/ - 迁移页面组件到
app/routes/ - 把
useEffect + fetch改成loader - 把表单处理改成
action - 测试所有路由是否正常工作
- 测试数据加载是否正常
- 测试表单提交是否正常
常见问题解答
Q: 我应该选哪个模式? A: 如果你是新项目,用 Framework Mode;如果你有现有项目要迁移,先用 Library Mode。
Q: 从 v6 迁移到 Library Mode 难吗? A: 不难。只需要改包名,其他代码基本不用动。
Q: 从 Library Mode 升级到 Framework Mode 难吗? A: 有一定难度。需要重新组织项目结构,把数据获取和表单处理改成新的方式。
Q: 我能同时用两种模式吗? A: 不能。一个项目只能用一种模式。
Q: Framework Mode 需要服务器吗? A: 是的。Framework Mode 需要 Node.js 服务器来运行 SSR。
Q: Library Mode 需要服务器吗? A: 不需要。Library Mode 是纯客户端路由,可以部署到任何静态托管服务。
Q: 我能在现有项目中逐步迁移到 Framework Mode 吗? A: 不建议。两种模式的项目结构完全不同,建议新建项目迁移。
选择建议
如果你是新手:
- 先学 Library Mode,理解路由的基本概念
- 再学 Framework Mode,了解全栈开发
如果你有现有项目:
- 先用 Library Mode 迁移,保持项目稳定
- 等熟悉了 v7 的新特性,再考虑 Framework Mode
如果你是新项目:
- 直接用 Framework Mode,一步到位
- 这是 React Router 的未来方向
个人建议:除非你有强烈的 SSR 需求,否则先用 Library Mode。等你熟悉了 v7 的新特性,再考虑 Framework Mode。
总结
- 路由的本质:让 URL 和 UI 保持同步,解决单页应用的 URL "失联"问题
- BrowserRouter:使用 History API,URL 更干净,需要服务器配合
- 嵌套路由:用 Outlet 组件实现布局复用,避免重复代码
- 动态路由 :用
:paramName定义路径参数,一个路由处理所有同类页面 - 编程式导航:用 useNavigate 在代码里控制跳转,适合登录、表单提交等场景
- Framework Mode:React Router v7 的全栈模式,内置 SSR、数据加载、文件路由
- Library Mode:传统的客户端路由模式,适合现有项目迁移
- 选择建议:新项目用 Framework Mode,现有项目用 Library Mode
相关资源
本文基于 React Router v7 版本