下载工作台
前端框架精通

React Hooks 与 React Router

试读上半部分 · 解锁后可读全文

第 5 章 · React Hooks 与 React Router

本章目标:掌握 React 核心 HooksuseStateuseEffectuseMemouseCallbackuseRef);编写自定义 Hook(useDebounceuseLocalStorage);用 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 }])
对比 VueReact
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 iddebounce 内部
避免闭包陈旧存最新 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 方案。

5.6.2 useLocalStorage

以下内容需解锁后阅读

试读已结束。解锁本章 ¥5.00,或开通年度会员畅读全部教程。
年度会员 ¥199.00/年; 小紫 AI 工作台有效会员 ¥99.00/年

正文仅在服务端鉴权后下发,未付费无法获取下半部分内容。