React Router:从旧版路由迁移到现代路由
本文目录
本文保留旧笔记的归档日期,内容按 React Router v8 重写。官方变更记录截至 2026-10-20 已列出 v8.5.0。示例适用于新建或已升级到 v8 的项目;维护旧项目时,先查看 package.json 与 lockfile 中的实际版本,不要只改 import 就直接复制代码。
先选路由模式
React Router 当前有三种逐步增强的模式:
| 模式 | 适合场景 | 能力 |
|---|---|---|
| Declarative(声明式) | 页面少、数据请求已有自己的方案 | URL 匹配、链接、导航和活动状态 |
| Data(数据) | 希望路由统一管理请求、提交和加载状态 | 在声明式路由上增加 loader、action、错误边界和待处理状态 |
| Framework(框架) | 希望由路由框架组织构建、类型、代码分割和渲染 | 在 Data 能力上增加路由模块、Vite 集成及 SPA/SSR/预渲染方案 |
不要为了“用了路由”就一开始把三种模式的代码混在一起。下面先用 Declarative 解释 URL 与组件的对应关系;项目确实需要路由级数据生命周期时,再看 Data 模式。
声明式路由
React Router v8 的一般 API 从 react-router 导入。安装包是 react-router:
v8 的最低运行基线是 Node 22.22+、React/React DOM 19.2.7+;Framework 模式还要求 Vite 7+。升级前先核对项目运行时和构建插件兼容性。只做声明式路由的项目不需要因此迁移到 Framework 模式。
npm install react-router
下面展示首页、项目列表、项目详情、嵌套布局和兜底页面:
import {
BrowserRouter,
Link,
NavLink,
Outlet,
Route,
Routes,
useParams,
} from "react-router";
export default function App() {
return (
<BrowserRouter>
<nav aria-label="主导航">
<NavLink to="/" end>首页</NavLink>
<NavLink to="/projects">项目</NavLink>
</nav>
<Routes>
<Route path="/" element={<Home />} />
<Route path="projects" element={<ProjectsLayout />}>
<Route index element={<ProjectList />} />
<Route path=":projectId" element={<ProjectDetail />} />
</Route>
<Route path="*" element={<NotFound />} />
</Routes>
</BrowserRouter>
);
}
function ProjectsLayout() {
return (
<main>
<h1>项目</h1>
<Outlet />
</main>
);
}
function ProjectList() {
return <Link to="42">打开项目 42</Link>;
}
function ProjectDetail() {
const { projectId } = useParams();
return <h2>项目编号:{projectId}</h2>;
}
function Home() {
return <h1>首页</h1>;
}
function NotFound() {
return <h1>页面不存在</h1>;
}
关键点:
- BrowserRouter 使用浏览器 History API。需要带 # 的 URL 时可改用 HashRouter,但它不是部署配置的替代品。
- Routes 根据当前地址选择匹配的路由。多个路由并不依赖手工排序来实现“第一个匹配”;具体匹配由路由树和路径优先级决定。
- 父路由组件要用 Outlet 指定子路由的渲染位置。index 子路由在父路由的 URL 下显示,不额外增加路径。
- 子路由路径通常相对父路径编写,例如 projects/:projectId 的子路径是 :projectId。动态片段可以通过 useParams() 读取。
- NavLink 会暴露活动状态,适合导航高亮;普通跳转用 Link,保留键盘操作、打开新标签页和复制链接等浏览器行为。
- useSearchParams() 适合筛选、分页等需要分享或刷新后保留的状态;location.state 适合临时导航上下文,不应当作可分享的数据存储。
在操作完成后编程式跳转
用户点击导航时优先用 Link 或 NavLink。只有在表单成功提交、登出等操作完成后需要自动跳转时,才使用 useNavigate():
import { useNavigate } from "react-router";
function CreateProjectButton() {
const navigate = useNavigate();
async function createProject() {
const project = await saveProject();
navigate("/projects/" + project.id);
}
return <button onClick={createProject}>创建项目</button>;
}
Data 模式:把数据生命周期放到路由
当页面数据需要与路由切换协同、需要统一的提交状态或错误边界时,可以使用 Data Router。v8 的 DOM 项目从 react-router/dom 导入 RouterProvider,其他路由 API 仍从 react-router 导入:
import ReactDOM from "react-dom/client";
import { createBrowserRouter, Outlet, useLoaderData } from "react-router";
import { RouterProvider } from "react-router/dom";
async function projectLoader({ params, request }) {
const response = await fetch(
"/api/projects/" + encodeURIComponent(params.projectId),
{ credentials: "same-origin", signal: request.signal },
);
if (!response.ok) {
throw new Response("项目加载失败", { status: response.status });
}
return response.json();
}
function ProjectPage() {
const project = useLoaderData();
return <h1>{project.name}</h1>;
}
function RootLayout() {
return <main><Outlet /></main>;
}
function Home() {
return <h1>首页</h1>;
}
function ProjectError() {
return <p role="alert">暂时无法加载项目,请稍后重试。</p>;
}
const router = createBrowserRouter([
{
path: "/",
Component: RootLayout,
children: [
{ index: true, Component: Home },
{
path: "projects/:projectId",
loader: projectLoader,
Component: ProjectPage,
ErrorBoundary: ProjectError,
},
],
},
]);
ReactDOM.createRoot(document.getElementById("root")).render(
<RouterProvider router={router} />,
);
createBrowserRouter() 应在组件树外创建一次,不要在组件每次 render 时重建。Data 模式还提供 action、Form、useFetcher 等能力,可用于路由提交、局部变更和待处理状态;如果只是简单展示页面,不必为了使用这些 API 而迁移整个应用。
登录路由不是权限边界
路由守卫可以把未登录用户带到登录页、登录后返回原页面,改善浏览体验。但守卫代码运行在浏览器中,用户可以修改状态、直接请求接口或绕过页面,因此它不能保护数据或接口。
真正的授权必须在服务端执行:每个受保护的 API、下载地址和数据读取都要验证会话及资源权限。客户端的路由判断只是界面控制;不要把 localStorage 中一个布尔值或示例里的 fakeAuth 当作身份认证。
在声明式路由中,可以用一个父路由集中组织需要登录的页面,并在其组件中根据应用现有的会话状态渲染 Outlet 或跳转。登录成功后的回跳地址必须限制为站内路径,防止开放重定向;服务端仍需独立拒绝未授权请求。
从 v5 迁移时的 API 对照
这张表用于定位旧代码,不表示所有 API 都能机械替换:
| v5 常见写法 | v6、v7、v8 常见方向 | 迁移时注意 |
|---|---|---|
| Switch | Routes | 路由嵌套与匹配模型有变化,不是单纯改组件名 |
| component / render | element,或对象路由里的 Component | element 接收 React 元素;Component 是 Data Router 路由对象的组件字段 |
| Redirect | Navigate 或 loader/action 中的 redirect | 声明式导航和数据请求中的重定向场景不同 |
| useHistory | useNavigate | 普通链接仍优先使用 Link |
| useRouteMatch | 按需求使用 useMatch、useParams 或 useLocation | 先明确需要匹配结果、路径参数还是当前位置 |
| exact | 通常不再需要 | 现代路由通过嵌套结构和路径优先级表达匹配 |
升级不能只做全局文本替换。逐页检查嵌套路由、参数读取、跳转状态、404、数据加载和表单提交,并以该项目锁定的主版本为准。
v8 的包导入边界
React Router v8 移除了 react-router-dom 这个兼容重导出包。一般 API(例如 Link、Route、useNavigate、createBrowserRouter)从 react-router 导入;DOM 专用的 RouterProvider 从 react-router/dom 导入。v6/v7 项目仍可能使用 react-router-dom,升级前先按官方升级指南迁移并检查依赖,不要把 v8 import 用到尚未升级的项目里。
部署和测试清单
使用 BrowserRouter 时,生产 Web 服务器需要把未知的前端页面路径回退到应用入口 HTML,否则刷新 /projects/42 可能直接得到服务器 404。API 路径和静态资源不能被错误地回退成 HTML。若站点部署在子路径,还要为路由器设置正确的 basename。使用 HashRouter 可以避免服务器处理页面路径,但会把路由放进 URL 片段中。
至少验证以下场景:
- 首页、嵌套页面、动态参数、查询参数和不存在的路径。
- 从列表进入详情、刷新详情深链、浏览器前进/后退。
- 未登录时的登录回跳、登录成功后的回跳,以及服务端对未授权 API 的拒绝。
- 生产环境的深链刷新、子路径部署和 API 404,确认没有把 API 响应误返回成入口 HTML。
- 加载失败和提交失败时的错误与待处理状态;Data 模式还需覆盖 loader、action 和错误边界。
动效是可选层,不要让动画影响路径状态、焦点管理或浏览器返回行为;同时尊重 prefers-reduced-motion。
官方资料
Live Notes
留言提交后会先进入审核,审核通过后对其他读者公开。