第 5 章 · React Hooks 与 React Router
本章目标:掌握 React 核心 Hooks(useState、useEffect、useMemo、useCallback、useRef);编写自定义 Hook(useDebounce、useLocalStorage);用 react-router-dom v6 实现多页导航;完成待办列表 Hook 实战。
学时建议:4~5 小时
前置:完成本模块 ch04;了解 Vue ch03 路由与状态(便于对照)。
5.1 Hooks 规则
| 规则 | 说明 |
|---|---|
| 只在顶层调用 | 不在 if/for/嵌套函数里调用 Hook |
| 只在 React 函数中调用 | 组件或自定义 Hook(use 开头) |
| 依赖数组诚实填写 | useEffect / useMemo / useCallback |
违反规则会导致状态错乱、闭包陈旧值——ESLint react-hooks/exhaustive-deps 务必开启。
npm install react-router-dom
5.2 useState:组件状态
const [count, setCount] = useState(0)
const [todos, setTodos] = useState(() =>
JSON.parse(localStorage.getItem('academy-todos') || '[]')
)
// 函数式更新 — 依赖上一次 state 时必用
setCount((c) => c + 1)
setTodos((prev) => [...prev, { id: crypto.randomUUID(), text, done: false }])
| 对比 Vue | React |
|---|---|
ref(0) | useState(0) |
count.value++ | setCount(c => c + 1) |
| 自动批量更新 | React 18 自动批处理(含 async) |
不可变更新:对象/数组必须返回新引用,直接 push 不会触发重渲染。
5.3 useEffect:副作用
useEffect(() => {
document.title = `${todos.filter((t) => !t.done).length} 项待办 · 贤紫学院`
}, [todos])
useEffect(() => {
let cancelled = false
async function load() {
const data = await fetch('/mock/courses.json').then((r) => r.json())
if (!cancelled) setCourses(data.courses ?? data)
}
load()
return () => { cancelled = true } // 清理:防竞态
}, []) // 空依赖 = 仅挂载执行
| 场景 | 依赖 | 清理 |
|---|---|---|
| 订阅 / 定时器 | [id] | return () => clearInterval |
| 同步 document | [title] | 通常无需 |
| Fetch 数据 | [] 或 [courseId] | abort / cancelled 标志 |
对比 Vue:onMounted + watch + onUnmounted 的组合。
5.4 useMemo 与 useCallback
5.4.1 useMemo — 缓存计算结果
const filtered = useMemo(() => {
const q = keyword.trim().toLowerCase()
return courses.filter((c) =>
!q || c.title.toLowerCase().includes(q)
)
}, [courses, keyword])
等价 Vue computed;仅当依赖变化时重算。
5.4.2 useCallback — 缓存函数引用
const handleToggle = useCallback((id) => {
setTodos((prev) =>
prev.map((t) => (t.id === id ? { ...t, done: !t.done } : t))
)
}, [])
// 子组件用 React.memo 时,稳定 onToggle 避免无效重渲染
<TodoItem todo={t} onToggle={handleToggle} />
| Hook | 缓存什么 | 何时用 |
|---|---|---|
useMemo | 值 | 昂贵 filter / sort |
useCallback | 函数 | 传给 memo 子组件的回调 |
过度优化警告:列表 < 100 项可先不写;先正确再优化。
5.5 useRef:DOM 与可变值
const inputRef = useRef(null)
function focusInput() {
inputRef.current?.focus()
}
// 跨渲染保存值,改 .current 不触发重渲染
const renderCount = useRef(0)
useEffect(() => {
renderCount.current += 1
})
| 用途 | 示例 |
|---|---|
| 聚焦输入框 | 待办页加载后 focus |
| 保存 timer id | debounce 内部 |
| 避免闭包陈旧 | 存最新 callback(进阶) |
5.6 自定义 Hook
自定义 Hook 抽取可复用状态逻辑,命名必须以 use 开头。
5.6.1 useDebounce
// src/hooks/useDebounce.js
import { useState, useEffect } from 'react'
export function useDebounce(value, delay = 300) {
const [debounced, setDebounced] = useState(value)
useEffect(() => {
const timer = setTimeout(() => setDebounced(value), delay)
return () => clearTimeout(timer)
}, [value, delay])
return debounced
}
课程列表搜索:
const [keyword, setKeyword] = useState('')
const debouncedQ = useDebounce(keyword, 300)
const filtered = useMemo(() => {
const q = debouncedQ.trim().toLowerCase()
return courses.filter((c) => !q || c.title.toLowerCase().includes(q))
}, [courses, debouncedQ])
对齐 ch16 debounce.js 与 Vue ch03 watch 方案。