本文是「前端路由完全指南」系列的中篇。我们将逐一拆解 React Router 的 8 个核心 Hook,并延伸到 React 状态管理和与 Vue 的框架对比。
系列文章:
- 上篇:前端路由原理与 6 种路由模式
- 中篇(本文):React Router Hooks 完全指南
- 下篇:懒加载、History 栈与工程化实践
前言:Hook 是什么?
在深入路由 Hook 之前,先用一句话理解 Hook:
Hook 就是让函数组件拥有"超能力"的函数。 它们都以
use开头。
在 Hook 出现之前,函数组件只能接收 props 然后渲染 JSX------它没有"记忆",不能管理状态,不能处理副作用。有了 Hook 之后:
| Hook | 给组件什么能力 |
|---|---|
useState |
记忆数据 --- 数据变了,组件自动重渲染 |
useEffect |
安排副作用 --- 渲染后执行操作(请求、定时器、DOM 操作) |
useRef |
保存引用 --- 记住一个值但不触发重渲染 |
useParams |
读 URL 参数 --- 从路径中提取 :id |
useNavigate |
编程式跳转 --- 在逻辑代码中切换页面 |
本文聚焦于路由相关的 Hook,但也会顺带讲解 React 状态管理的基础,因为这是理解整个体系不可或缺的一环。
一、路由 Hook 全景速查
React Router v7 提供了 8 个常用 Hook,按使用频率排列:
| Hook | 作用 | 返回值 |
|---|---|---|
useNavigate() |
编程式页面跳转 | 导航函数 |
useParams() |
读取 URL 路径参数 | { id: "123" } |
useLocation() |
读取当前完整 URL 信息 | { pathname, search, hash, state } |
useSearchParams() |
读写查询参数 | [params, setter] |
useMatch() |
判断当前 URL 是否匹配某路径 | match 对象或 null |
useResolvedPath() |
将路径字符串解析为 Location 对象 | { pathname, search, hash } |
useOutletContext() |
嵌套路由父子传参 | context 对象 |
下面逐一详解。
二、useNavigate --- 编程式导航
什么时候用?
当你需要在 JS 逻辑中跳转页面时------比如表单提交成功后跳转、倒数计时后跳转。
jsx
import { useNavigate } from 'react-router-dom'
function NotFound() {
const navigate = useNavigate()
useEffect(() => {
setTimeout(() => {
navigate('/') // 3 秒后跳回首页
}, 3000)
}, [])
return <h1>404 Not Found</h1>
}
push vs replace
jsx
navigate('/about') // push 模式(默认):追加历史记录
navigate('/about', { replace: true }) // replace 模式:替换当前记录
| 模式 | 对历史栈的影响 | 后退按钮 |
|---|---|---|
| push(默认) | 新记录追加到栈顶 | 能回到上一页 |
| replace | 替换当前栈顶 | 跳过当前页 |
一个典型场景: 鉴权拦截跳转到登录页,必须用 replace,否则用户点后退会陷入"未登录→重定向→后退→重定向"的死循环。
Navigate 组件 vs useNavigate Hook
jsx
// 声明式:写在 JSX 里,渲染即跳转
<Navigate to="/login" />
// 命令式:在 JS 逻辑中调用
navigate('/login')
<Navigate> |
useNavigate() |
|
|---|---|---|
| 类型 | 组件 | Hook |
| 何时触发 | 渲染时立即跳转 | 主动调用时跳转 |
| 典型场景 | 路由配置中的重定向、守卫 | 事件回调、异步操作后的跳转 |
三、useParams --- 读取路径参数
jsx
// 路由定义:<Route path="/user/:id" element={<UserProfile />} />
// 浏览器访问:/user/123
import { useParams } from 'react-router-dom'
function UserProfile() {
const { id } = useParams() // id = "123"
return <h1>用户 {id}</h1>
}
多个参数也一样:
jsx
// 路由:/product/:category/:productId
// URL: /product/electronics/456
const { category, productId } = useParams()
// category = "electronics", productId = "456"
四、useMatch --- 路径匹配判断
基本用法
判断当前浏览器 URL 是否匹配某个路径:
jsx
import { useMatch } from 'react-router-dom'
function Navigation() {
// 当前在 /about → 返回 match 对象
// 当前在 /home → 返回 null
const match = useMatch('/about')
return (
<Link to="/about" className={match ? 'active' : ''}>
关于我们
</Link>
)
}
end 参数:精确匹配 vs 前缀匹配
jsx
useMatch({ path: '/', end: true }) // 精确匹配:/ ✅ /about ❌
useMatch({ path: '/', end: false }) // 前缀匹配:/ ✅ /about ✅ /a/b ✅
假设当前 URL 是 /products/123:
| 比对路径 | end: true(精确) |
end: false(前缀) |
|---|---|---|
/ |
❌ | ✅(所有 URL 都以 / 开头) |
/products |
❌ | ✅ |
/products/123 |
✅ | ✅ |
为什么导航高亮必须用 end: true? 因为 / 是所有 URL 的前缀,不用精确匹配的话,首页链接会在每个页面都高亮。
五、实战:导航高亮是怎么实现的?
这是 useMatch 和 useResolvedPath 组合使用的经典案例:
jsx
import { useResolvedPath, useMatch, Link } from 'react-router-dom'
function Navigation() {
const isActive = (to) => {
// ① 将传入路径解析为完整的 Location 对象
// 例如 '/about' → { pathname: '/about', search: '', hash: '' }
const resolvedPath = useResolvedPath(to)
// ② 将当前浏览器 URL 与目标路径做比对
const match = useMatch({
path: resolvedPath.pathname,
end: true // 精确匹配
})
// ③ 匹配成功返回 CSS 类名,失败返回空字符串
return match ? 'active' : ''
}
return (
<nav>
<Link to="/" className={isActive('/')}>首页</Link>
<Link to="/about" className={isActive('/about')}>关于</Link>
<Link to="/products" className={isActive('/products')}>产品</Link>
</nav>
)
}
配合一行 CSS 就能实现高亮效果:
css
.active { color: red; }
两个 API 的分工
| API | 作用 |
|---|---|
useResolvedPath(to) |
把相对路径 /about 解析为绝对路径对象 { pathname: '/about', ... } |
useMatch({ path, end }) |
判断当前 URL 是否命中指定路径 |
六、useLocation --- 读取完整 URL 信息
jsx
import { useLocation } from 'react-router-dom'
const location = useLocation()
// → { pathname: '/user/123', search: '?tab=posts', hash: '#intro', state: {...} }
常用场景:
1. 读取导航传递的 state:
jsx
// 跳转时携带数据
navigate('/pay', { state: { from: '/products', productId: 123 } })
// 目标页面读取
const location = useLocation()
console.log(location.state) // { from: '/products', productId: 123 }
2. 面包屑导航:
jsx
const { pathname } = useLocation()
const segments = pathname.split('/').filter(Boolean)
// /products/123/edit → ['products', '123', 'edit']
3. 页面埋点 / 统计:
jsx
useEffect(() => {
analytics.pageView(location.pathname)
}, [location])
七、useSearchParams --- 读写查询参数
jsx
import { useSearchParams } from 'react-router-dom'
function ProductList() {
const [searchParams, setSearchParams] = useSearchParams()
// 读取
const keyword = searchParams.get('keyword') // ?keyword=phone → "phone"
const page = searchParams.get('page') || '1' // 带默认值
// 写入(不刷新页面)
const goToPage = (n) => {
setSearchParams({ keyword, page: String(n) })
// URL 变为 ?keyword=phone&page=2
}
return (
<>
<input value={keyword} onChange={...} />
<button onClick={() => goToPage(2)}>第 2 页</button>
</>
)
}
URL 各部分对应谁处理
bash
https://example.com/user/123?tab=posts&page=2#comment
└──── 域名 ────┘ └─路径─┘ └── 查询参数 ──┘ └hash┘
↑ ↑ ↑
useParams useSearchParams 浏览器锚点
八、useOutletContext --- 嵌套路由父子传参
父路由可以通过 <Outlet context={...} /> 向子路由传递数据,子路由用 useOutletContext() 接收:
jsx
// 父组件
function Product() {
const [products, setProducts] = useState([])
return (
<>
<h1>产品列表</h1>
<Outlet context={{ products, setProducts }} /> {/* 传数据给子路由 */}
</>
)
}
// 子组件
function ProductDetail() {
const { products } = useOutletContext() // 接收父组件传来的数据
// ...
}
这是嵌套路由场景下替代 props 传递的方式,因为子路由组件不是父组件直接写在 JSX 里的,无法直接传 props。
九、延伸:React 状态管理基础
路由 Hook 解决的是"URL 和组件之间的映射"。但一个完整的应用还需要管理组件内部的数据状态。这里用最简单的方式说明。
useState --- 让组件有"记忆"
jsx
import { useState } from 'react'
function Counter() {
const [count, setCount] = useState(0) // 初始值 0
return (
<div>
<p>计数:{count}</p>
<button onClick={() => setCount(count + 1)}>+1</button>
</div>
)
}
useState 返回一个数组:[当前值, 修改函数]。调用 setCount 后,React 会自动重新渲染组件,显示新的值。
useEffect --- 处理"副作用"
渲染之外的操作都叫"副作用":请求数据、设置定时器、操作 DOM、订阅事件。
jsx
import { useState, useEffect } from 'react'
function UserPage({ userId }) {
const [user, setUser] = useState(null)
useEffect(() => {
// 组件渲染后执行:请求用户数据
fetch(`/api/user/${userId}`)
.then(res => res.json())
.then(data => setUser(data))
}, [userId]) // userId 变化时重新执行
if (!user) return <div>加载中...</div>
return <div>{user.name}</div>
}
React 数据流:单向绑定
React 的数据是单向流动 的:state → UI。想改变 UI,必须先改变 state。
jsx
// React:受控组件模式
const [name, setName] = useState('')
<input
value={name} // state → 输入框
onChange={(e) => setName(e.target.value)} // 输入框 → state
/>
这需要比 Vue 多写一些代码,但换来的是数据变化永远有迹可循 ------每次更新都是显式调用 setName 的结果。
对比:Vue 的双向绑定
vue
<!-- Vue:v-model 一行搞定 -->
<input v-model="name" /> <!-- 输入框 ⟷ name,自动双向同步 -->
| React | Vue | |
|---|---|---|
| 数据流 | 单向(state → UI) | 双向(state ⟷ UI) |
| 更新方式 | 显式 setState |
v-model 自动同步 |
| 调试 | 每条更新都可追踪 | 需要 devtools 协助 |
| 代码量 | 多 | 少 |
| 理念 | "一切变化都要显式声明" | "数据绑定自动完成" |
没有谁更好,只是设计哲学不同。
十、自定义 Hook --- 封装可复用逻辑
导航高亮逻辑在多个组件中可能都会用到。我们可以把它抽成一个自定义 Hook:
jsx
// hooks/useActive.js
import { useResolvedPath, useMatch } from 'react-router-dom'
export function useActive(to) {
const resolvedPath = useResolvedPath(to)
const match = useMatch({ path: resolvedPath.pathname, end: true })
return match ? 'active' : ''
}
使用起来一行搞定:
jsx
<Link to="/" className={useActive('/')}>首页</Link>
<Link to="/about" className={useActive('/about')}>关于</Link>
自定义 Hook 的规则
- 函数名必须以
use开头(这是 React 识别它的方式) - 内部可以调用其他 Hook
- 本质上就是把多个 Hook 的组合逻辑打包成一个函数
| 普通函数 | 自定义 Hook |
|---|---|
| 不能调 Hook | 可以调 Hook |
| 无命名约束 | 必须以 use 开头 |
| 纯逻辑计算 | 封装可复用的状态/路由逻辑 |
总结:一句话记忆所有 Hook
取参用
useParams,跳转用useNavigate,读 URL 用useLocation,查参数用useSearchParams,匹配路径用useMatch,解析路径用useResolvedPath,父子传参用useOutletContext。
中篇我们掌握了所有路由 Hook 的用法,也理解了 React 状态管理的基础------useState 让组件有记忆,useEffect 处理副作用,自定义 Hook 封装复用逻辑。下篇我们将进入工程实践:路由懒加载怎么做到首屏秒开?push 和 replace 在什么场景下用哪个?项目的文件结构怎么组织才清晰?