前端路由完全指南(中篇):React Router Hooks 完全指南

本文是「前端路由完全指南」系列的中篇。我们将逐一拆解 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,否则用户点后退会陷入"未登录→重定向→后退→重定向"的死循环。

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 的前缀,不用精确匹配的话,首页链接会在每个页面都高亮。


五、实战:导航高亮是怎么实现的?

这是 useMatchuseResolvedPath 组合使用的经典案例:

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 在什么场景下用哪个?项目的文件结构怎么组织才清晰?

相关推荐
hunterandroid1 小时前
[鸿蒙从零到一] ArkUI 声明式 UI 基础实战
前端
七牛开发者1 小时前
RAG 工程里的上下文剪枝:如何减少 68% 的上下文
前端·css·github
橘子星2 小时前
从零理解流式输出 —— 一个 Vue + DeepSeek 的前端实战
前端·javascript·人工智能
智驭未来掌门人2 小时前
如何让你的前端服务在服务器上nohub运行
前端
颜进强2 小时前
Claude Code - 18 从 Figma 到 React:一套让 AI 生成代码"不瞎编"的实战工作流
前端·后端·ai编程
李伟_Li慢慢2 小时前
12-mini版WebGLObjects
前端·three.js
李伟_Li慢慢2 小时前
17-mini版WebGLBufferRenderer
前端·three.js
李伟_Li慢慢2 小时前
16-mini版WebGLBackground
前端·three.js