React Router从入门到进阶
英文官网地址:
https://reactrouter.com/home
下面示例代码全部基于 v6 最新稳定 API。
思维导图:
React Router v6
├─ 入口模式
│ ├─ Data Router (createBrowserRouter → RouterProvider) ⭐ 推荐
│ ├─ 组件式 (BrowserRouter → Routes → Route)
│ ├─ Hash (createHashRouter)
│ └─ Memory (createMemoryRouter) ➜ 测试/SSR/RN
├─ 导航
│ ├─ 声明式: Link / NavLink
│ └─ 命令式: useNavigate / redirect (in loader/action)
├─ 参数
│ ├─ 路径参数: useParams
│ └─ 查询参数: useSearchParams
├─ 数据
│ ├─ 预加载: loader + useLoaderData
│ ├─ 写数据: action + useActionData / useSubmit / <Form>
│ └─ 独立请求: useFetcher
└─ 错误
└─ errorElement + useRouteError
一、入口配置的不同写法说明
1.Data Router
对象配置,官方推荐,支持 loader、action、errorBoundary
// main.jsx
import ReactDOM from 'react-dom/client'
import { createBrowserRouter, RouterProvider } from 'react-router-dom'
import Home from './pages/Home'
import About from './pages/About'
const router = createBrowserRouter([
{ path: '/', element: <Home /> },
{ path: '/about', element: <About /> }
])
/*
* createBrowserRouter 创建路由,支持 loader/action/errorElement
* RouterProvider 把 router 实例注入 React 树,内部用 context 向下传递
* router 不可变对象,换路由=换树,React 18 可配合 Streaming SSR
*/
ReactDOM.createRoot(document.getElementById('root')).render(
<RouterProvider router={router} />
)
// 官网路由对象说明:https://reactrouter.com/start/data/route-object
interface RouteObject {
path?: string; // 匹配路径,例 "/home", "/users/:id"
index?: boolean; // 是否为父路由的默认索引路由(index=true 时 path 被忽略)
element?: React.ReactNode; // 要渲染的组件/元素,例 <Home />
Component?: React.ComponentType; // 同 element,但用类/函数名(官方推荐用 element)
children?: RouteObject[]; // 子路由,嵌套时用
loader?: (args: LoaderFunctionArgs) => Promise<Data> | Data; // 进入页面前拉数据
action?: (args: ActionFunctionArgs) => Promise<Data> | Data | Response | redirect; // 表单提交/写数据
errorElement?: React.ReactNode; // 当前路由或其 loader/action 抛错时展示的组件
ErrorBoundary?: React.ComponentType; // 同上,只是用类组件形式
handle?: any; // 自定义句柄,可放标题、权限、面包屑等任意元数据
id?: string; // 路由唯一 id,一般自动生成,也可手动指定
// 以下两个高级异步用到再看
lazy?: () => Promise<{ loader?: Loader; action?: Action; Component?: Component; etc... }>;
shouldRevalidate?: ShouldRevalidateFunction; // 决定是否重新执行 loader
}
2.组件式
声明式,适合小项目 / 渐进升级
// 组件式:完全用 JSX 写路由,无需 createBrowserRouter
import { BrowserRouter, Routes, Route } from 'react-router-dom'
root.render(
<BrowserRouter> {/* 创建 history,提供 context */}
<Routes> {/* 匹配唯一 Route,在v5版本中,使用Switch标签 */}
<Route path="/" element={<Home />} /> {/* 在v5版本中,会使用属性exact来表示完全匹配,避免访问访问 /about 时,Home也会渲染 */}
<Route path="/about" element={<About />} />
</Routes>
</BrowserRouter>
)
特点:
- 写起来直观,但 不支持 loader/action/errorElement(v6.4+ 新特性只能用 Data Router)。
- 源码层面 BrowserRouter 内部其实还是 createBrowserRouter 的封装 。
3.Hash Router
文件协议 / 旧服务器无法重定向时用
// Hash 模式
import { createHashRouter, RouterProvider } from 'react-router-dom'
const router = createHashRouter([ … ]) // URL 带 #,刷新 404 问题最小
root.render(<RouterProvider router={router} />)
4.Memory Router
单元测试 / React Native / SSR 无浏览器 history
// 4️⃣ 内存路由
import { createMemoryRouter, RouterProvider } from 'react-router-dom'
const router = createMemoryRouter([ … ], { initialEntries: ['/'] })
/* 测试示例
* render(<RouterProvider router={router}>)
* router.navigate('/about') // 不会改地址栏,纯内存
*/
二、常用Hook/组件介绍
| 名称 | 作用 | 只能在 Data Router 用? | 典型用法 |
|---|---|---|---|
useNavigate |
编程式跳转 | ❌ | const nav=useNavigate(); nav('/about',{replace:true}) |
useLocation |
拿到当前 location(pathname/search/hash) | ❌ | const {search}=useLocation() |
useParams |
取动态参数 :id |
❌ | const {id}=useParams() |
useSearchParams |
读/写 queryString(像 URLSearchParams) | ❌ | const [sp,setSp]=useSearchParams() |
useLoaderData |
获取当前路由 loader 的返回值 | ✅ | const data=useLoaderData() |
useActionData |
获取当前路由 action 的返回值(表单 POST 后) | ✅ | const msg=useActionData() |
useRouteError |
在 errorElement 内读取抛出的错误 | ✅ | const err=useRouteError() |
useSubmit |
编程式提交表单(走 action) | ✅ | const submit=useSubmit(); submit(data,{method:'post'}) |
useFetcher |
不切换路由、独立调用 action/loader | ✅ | const fetcher=useFetcher(); fetcher.load('/api') |
NavLink |
带“活跃状态”的 Link | ❌ | <NavLink to="/about" className={({isActive})=>isActive?'active':''}> |
useHistory是 React Router v5 的产物;useNavigate是 React Router v6 的替代品,API 更简洁、语义更清晰,同时支持“跳转”和“回退/替换”。
useHistory和useNavigate对比:
| 能力 | useHistory (v5) | useNavigate (v6) |
|---|---|---|
| 前进/普通跳转 | history.push('/path') |
navigate('/path') |
| 替换当前记录 | history.replace('/path') |
navigate('/path', {replace: true}) |
| 后退/前进 | history.go(-1) / history.back() |
navigate(-1)(数字即可) |
| 状态携带 | history.push('/path', {foo: 1}) |
navigate('/path', {state: {foo: 1}}) |
| 返回值类型 | history 对象(含监听、location 等) |
只有 navigate 函数,体积更小 |
| 阻塞跳转 | history.block('确认离开?') |
需用 useBlocker Hook(独立 API) |
三、Data Router独特功能介绍
1. loader
在进入路由前并行拉数据,返回值通过 useLoaderData 消费。
{
path: 'dashboard',
element: <Dashboard />,
loader: async () => {
const res = await fetch('/api/stat')
if (!res.ok) throw new Response('stat error', { status: 500 })
return res.json()
}
}
// src/pages/Dashboard.jsx
// ⬅️ 这里直接拿到 loader 的返回值,无需 useEffect 再请求
const statData = useLoaderData()
2. action
处理 POST/PUT/DELETE/PATCH(表单或 submit),返回值通过 useActionData 消费;支持重定向。
{
path: 'login',
element: <LoginPage />,
action: async ({ request }) => {
const body = await request.formData()
const user = await loginAPI(body)
if (!user) return { error: '账号错误' } // 返回数据
throw redirect('/') // 跳转
}
}
// 登录页组件(src/pages/LoginPage.jsx)
import { Form, useActionData, useSubmit } from 'react-router-dom'
export default function LoginPage() {
// ⬅️ 接收 action 返回值(成功时无值,失败时有 {error})
const actionData = useActionData()
/* 两种提交方式任选其一: */
// 方式 A:声明式 <Form>(推荐,自动序列化)
return (
<>
<h2>登录</h2>
{actionData?.error && <p style={{ color: 'red' }}>{actionData.error}</p>}
<Form method="post" action="/login">
<div>
<label>
用户名:
<input name="username" type="text" required />
</label>
</div>
<div>
<label>
密码:
<input name="password" type="password" required />
</label>
</div>
<button type="submit">登录</button>
</Form>
</>
)
// 方式 B:编程式 useSubmit(适合自定义校验/额外逻辑)
// const submit = useSubmit()
// const handleValidate = (e: FormEvent) => {
// e.preventDefault()
// const form = e.currentTarget
// const formData = new FormData(form)
// submit(formData, { method: 'post', action: '/login' })
// }
// 然后把 <Form> 换成 <form onSubmit={handleValidate}> 即可
}
3. errorElement
当前路由或它的 loader/action 抛错时渲染,代替整个白屏。
{
path: 'dashboard',
element: <Dashboard />,
loader: dashboardLoader,
errorElement: <ErrorBoundary /> // 内部用 useRouteError() 读错误
}
// 错误边界组件(src/components/ErrorBoundary.jsx)
import { useRouteError } from 'react-router-dom'
export default function ErrorBoundary() {
const error = useRouteError() // 读取错误对象(可以是 Error 或 Response)
// 区分 HTTP 错误 vs JS 运行时错误
const isResponse = error instanceof Response
return (
<div style={{ padding: 24, background: '#fff1f0', border: '1px solid #ffccc7' }}>
<h2>💥 出错了!</h2>
{isResponse ? (
<p>
网络异常:{error.status} {error.statusText}
</p>
) : (
<p>运行时错误:{error.message || error}</p>
)}
<details style={{ whiteSpace: 'pre-wrap' }}>
{isResponse
? '请检查接口或 loader 逻辑。'
: error.stack || '无堆栈信息'}
</details>
<button onClick={() => window.location.reload()}>刷新重试</button>
</div>
)
}
和React原生的异常处理区别
- React 原生
<ErrorBoundary>只能捕捉“渲染阶段”和“生命周期”里抛出的错误;对 异步代码(fetch、setTimeout)、事件回调、loader/action 无效。 - React Router 的
errorElement+useRouteError专门捕捉 路由层面 的 异步错误(loader/action 抛错、懒加载模块失败),同时也可顺带捕捉该路由组件渲染时的同步错误;但它 不跨路由级别冒泡到全局 React 树。
- 范围对比
| 场景 | React 原生 ErrorBoundary | React Router errorElement |
|---|---|---|
| 组件渲染 throw | ✅ 捕获 | ✅ 捕获 |
| 生命周期/componentDidCatch | ✅ 捕获 | ✅ 捕获 |
| 事件回调里 throw | ❌ 不捕获 | ❌ 不捕获(需自行 try/catch) |
| loader 里 throw | ❌ 捕获不到 | ✅ 专属捕获 |
| action 里 throw | ❌ 捕获不到 | ✅ 专属捕获 |
| 懒加载代码分割失败 | ✅ 捕获 | ✅ 捕获 |
| 跨整个 React 树(非路由) | ✅ 可以放在根 | ❌ 只影响当前路由分支 |
- 捕获时机
React 原生
- 同步渲染阶段 出现错误 → 触发
getDerivedStateFromError→ 渲染备用 UI。 - 异步错误(fetch 失败、setTimeout 回调里 throw)不会触发,需要你在异步代码里手动
setState触发渲染错误,或自己包 try/catch。
React Router
- 路由匹配前 运行 loader → 抛错 → 直接跳过原组件,渲染
errorElement。 - 路由匹配后 组件渲染抛错 → 同样落入
errorElement。 - 不care 事件回调里的错误;那种错误仍需要你用原生 ErrorBoundary 或自己处理。
- API 差异
React 原生(类组件)
class MyErrorBoundary extends React.Component {
static getDerivedStateFromError(error) {
return { hasError: true } // 更新 state 使下一次渲染备用 UI
}
componentDidCatch(error, info) {
console.error('全局错误:', error, info)
}
render() {
return this.state.hasError
? <p>页面崩溃啦</p>
: this.props.children
}
}
React Router(函数组件)
function RouterErrorBoundary() {
const error = useRouteError() // 读取 loader/action 或渲染错误
return <div>路由层错误:{error.message||error.status}</div>
}
// 路由表里挂上去
{ path: 'dashboard', element: <Dashboard />, errorElement: <RouterErrorBoundary /> }
- 使用场景速记
- 页面级“兜底”(按钮点击、定时器、非路由相关)→ 用 React 原生 ErrorBoundary 包在根或大块业务组件外。
- 数据加载/提交失败、路由懒加载失败 → 用 React Router errorElement 挂在对应路由,用户体验最好(URL 保持,只局部出错误卡片)。
- 两者可以同时存在:
全局包一个 React ErrorBoundary 防“未知渲染崩溃”,每个路由再放一个 errorElement 专门处理“数据/路由”错误,互不干扰。
开发实践
0. 环境准备
# 创建项目(vite 最快)
npm create vite@latest my-router --template react
cd my-router
npm i
# 安装路由
npm i react-router-dom@6
1. 基础:页面跳转
// main.jsx
import ReactDOM from 'react-dom/client'
import { createBrowserRouter, RouterProvider } from 'react-router-dom'
import Home from './pages/Home'
import About from './pages/About'
const router = createBrowserRouter([
{ path: '/', element: <Home /> },
{ path: '/about', element: <About /> }
])
/*
* createBrowserRouter 创建路由,支持 loader/action/errorElement
* RouterProvider 把 router 实例注入 React 树,内部用 context 向下传递
* router 不可变对象,换路由=换树,React 18 可配合 Streaming SSR
*/
ReactDOM.createRoot(document.getElementById('root')).render(
<RouterProvider router={router} />
)
// 跳转示例
/* pages/Home.jsx */
import { Link } from 'react-router-dom'
export default function Home() {
return (
<>
<h1>Home</h1>
<Link to="/about">跳转到 About</Link>
</>
)
}
// 跑通后,地址栏手动输入 `/about` 或点击 Link 都能切换,即最简 SPA 完成 。
2. 布局 + 嵌套路由(Layout + Outlet)
实际项目需要“公共头尾 + 中间换内容”。
// main.jsx 只改 router
const router = createBrowserRouter([
{
path: '/',
element: <Layout />, // 外壳
children: [ // 嵌套子路由
{ index: true, element: <Home /> }, // “/” 默认渲染
{ path: 'about', element: <About /> },
{ path: 'products', element: <Products /> }
]
}
])
/* components/Layout.jsx */
import { Outlet, Link } from 'react-router-dom'
export default function Layout() {
return (
<>
<nav>
<Link to="/">Home</Link> | <Link to="about">About</Link> | <Link to="products">Products</Link>
</nav>
<main style={{ padding: 16 }}>
<Outlet /> {/* 子路由会渲染到这里 */}
</main>
</>
)
}
此时访问 /products 只会局部刷新 <Outlet/> 区域,头部导航不闪屏 。
3. 动态路由 + 参数读取
// router 加一条
{ path: 'products/:id', element: <ProductDetail /> }
/* pages/ProductDetail.jsx */
import { useParams } from 'react-router-dom'
export default function ProductDetail() {
const { id } = useParams() // 取 /products/5 中的 5
return <h2>商品编号:{id}</h2>
}
Link 写法:<Link to="/products/5">iPhone 17</Link>
编程式跳转:
import { useNavigate } from 'react-router-dom'
const nav = useNavigate()
nav('/products/6') // 或 nav(-1) 后退
4. 404 与重定向
// 把下面两条放到路由表最后
{ path: 'old-about', element: <Navigate to="/about" replace /> },
{ path: '*', element: <NotFound /> } // 任何未匹配路径都会落到这里
5. 数据预加载(loader)——进阶第一步
React Router v6 自带“路由级数据预取”,无需 useEffect。
// router
{
path: 'dashboard',
element: <Dashboard />,
loader: async () => {
const res = await fetch('/api/stat')
return res.json()
}
}
/* pages/Dashboard.jsx */
import { useLoaderData } from 'react-router-dom'
export default function Dashboard() {
const data = useLoaderData() // 直接拿到 loader 返回值
return <div>今日 PV:{data.pv}</div>
}
好处:数据在渲染前就 Ready,配合 <Suspense> 可一键做骨架屏 。
6. 路由级懒加载(代码分割)
import { lazy, Suspense } from 'react'
const HeavyPage = lazy(() => import('./pages/HeavyPage'))
// 路由配置
{
path: 'heavy',
element: (
<Suspense fallback={<div>Loading...</div>}>
<HeavyPage />
</Suspense>
)
}
打包后 heavy.page.js 会被单独拆包,首屏加载体积瞬间减小 。
7. 权限守卫(高阶组件版)
function RequireAuth({ children }) {
const isLogin = !!localStorage.getItem('token')
return isLogin ? children : <Navigate to="/login" replace />
}
// 路由里这样用
{
path: 'admin',
element: (
<RequireAuth>
<Admin />
</RequireAuth>
)
}
更多推荐


所有评论(0)