「React Router v7 教程」从零到全栈,一篇搞定

「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 是 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 是 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-domreact-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 版本

相关推荐
wyc是xxs9 小时前
npm包推荐
前端·npm·node.js
programhelp_9 小时前
Ramp OA 四关全过,CodeSignal OOD 完整复盘
linux·前端·python
ZengLiangYi9 小时前
系统托盘 + 窗口状态持久化:Electron 细节
前端·electron
李铁蛋zs9 小时前
AI 前端开发 Prompt 模板库
前端·vue.js·prompt
Muen10 小时前
Swift-属性包装器
前端
qq_25183645710 小时前
基于java Web快乐岛儿童网站设计与实现
java·开发语言·前端
Crystal32810 小时前
App wgt 热更新 — 开发笔记(uniapp)
前端·uni-app·app
newAir10 小时前
前端转 AI 应用开发 · 02 | 5 分钟用 Python 调通大模型(async + 阿里云 Coding Plan)
前端·人工智能
来一碗刘肉面10 小时前
使用Tailwind CSS 创建一个新项目
前端·css
Ruihong10 小时前
VuReact v1.8.4 发布:Vue 迁移 React 编译器迎来稳定性大修,这些坑终于被填平了
前端·vue.js·react.js