React Hooks 设计模式与踩坑指南

2026.07.23· 约 1 分钟 ·tech·
#React
#Hooks
#设计模式

从 Class 到 Function:一场范式迁移

React 16.8 引入 Hooks 时,社区的反应两极分化——有人觉得「终于不用写 this 了」,有人觉得「这依赖数组是什么鬼」。但要真正理解 Hooks,你必须先接受一个认知:React 的函数组件不再只是"渲染函数",它变成了有状态的执行单元。

每次 re-render,你的组件函数都会重新执行一遍。Hooks 的魔法就在于:React 能在这一次次执行中记住你之前的状态。靠的是调用顺序——这也是「不能在条件语句里用 Hook」的根本原因。

useState 的隐藏细节

function Counter() {
  const [count, setCount] = useState(0)

  // ❌ 连续调用三次 setCount(count + 1),count 还是 1
  function wrongTriple() {
    setCount(count + 1)
    setCount(count + 1)
    setCount(count + 1)
  }

  // ✅ 用函数式更新,每次拿到最新值
  function correctTriple() {
    setCount(c => c + 1)
    setCount(c => c + 1)
    setCount(c => c + 1)
  }

  return <button onClick={correctTriple}>+3</button>
}

💡 原理: setCount(count + 1) 读取的是当前渲染帧的 count 值(闭包),三次调用读到的都是同一个值。函数式更新 c => c + 1 则每次都基于最新值计算。当新状态依赖旧状态时,一定要用函数式更新。

懒初始化

// ❌ 每次 re-render 都执行一次复杂计算,尽管只有初次渲染用得到
const [data, setData] = useState(expensiveComputation())

// ✅ useState 支持传入初始化函数,只会执行一次
const [data, setData] = useState(() => expensiveComputation())

useEffect 与闭包陷阱

这是 React Hooks 最大的坑,没有之一。

function Timer() {
  const [count, setCount] = useState(0)

  useEffect(() => {
    const id = setInterval(() => {
      // 🐛 Bug! count 永远是 0
      console.log(count)
      setCount(count + 1)
    }, 1000)
    return () => clearInterval(id)
  }, []) // 空依赖 = 只在 mount 时执行
}

为什么 count 永远是 0? 因为 useEffect 的回调在 mount 时创建,它捕获了当时的 count = 0。之后每次 re-render 产生的新闭包里 count 是不同的,但定时器一直用的是最初那个闭包。

解决方案一:补充依赖

useEffect(() => {
  const id = setInterval(() => {
    setCount(c => c + 1) // 函数式更新,不读 count
  }, 1000)
  return () => clearInterval(id)
}, []) // 空依赖也 OK!

解决方案二:useRef 桥接

function Timer() {
  const [count, setCount] = useState(0)
  const countRef = useRef(count)
  countRef.current = count // 始终保持最新值

  useEffect(() => {
    const id = setInterval(() => {
      console.log(countRef.current) // ✅ 总是最新值
    }, 1000)
    return () => clearInterval(id)
  }, [])
}

自定义 Hook:代码复用的真正答案

React 世界经历了一条清晰的复用演化路线:Mixin(混乱)→ HOC(嵌套地狱)→ Render Props(啰嗦)→ Custom Hooks(简洁)。

useDebounce:防抖搜索

function useDebounce<T>(value: T, delay: number): T {
  const [debouncedValue, setDebouncedValue] = useState(value)

  useEffect(() => {
    const timer = setTimeout(() => setDebouncedValue(value), delay)
    return () => clearTimeout(timer)
  }, [value, delay])

  return debouncedValue
}

useLocalStorage:持久化状态

function useLocalStorage<T>(key: string, initialValue: T) {
  const [storedValue, setStoredValue] = useState<T>(() => {
    try {
      const item = window.localStorage.getItem(key)
      return item ? JSON.parse(item) : initialValue
    } catch {
      return initialValue
    }
  })

  const setValue = (value: T | ((val: T) => T)) => {
    const valueToStore = value instanceof Function ? value(storedValue) : value
    setStoredValue(valueToStore)
    window.localStorage.setItem(key, JSON.stringify(valueToStore))
  }

  return [storedValue, setValue] as const
}

useWindowSize:响应式窗口尺寸

function useWindowSize() {
  const [size, setSize] = useState({ width: 0, height: 0 })

  useEffect(() => {
    function handleResize() {
      setSize({ width: window.innerWidth, height: window.innerHeight })
    }
    window.addEventListener('resize', handleResize)
    handleResize()
    return () => window.removeEventListener('resize', handleResize)
  }, [])

  return size
}

看到模式了吗?自定义 Hook = 把有状态的逻辑提取出来,在多个组件间共享。它共享的是逻辑,不是状态本身——每个调用 useDebounce 的组件都有自己独立的 timer 和 debouncedValue。

useReducer:当 useState 不够用

一个判断标准:如果你的下一个状态依赖前一个状态,而且逻辑比较复杂,就该上 useReducer。

type Action =
  | { type: 'ADD_TODO'; text: string }
  | { type: 'TOGGLE'; id: number }
  | { type: 'DELETE'; id: number }

function todoReducer(state: Todo[], action: Action): Todo[] {
  switch (action.type) {
    case 'ADD_TODO':
      return [...state, { id: Date.now(), text: action.text, done: false }]
    case 'TOGGLE':
      return state.map(t => t.id === action.id ? { ...t, done: !t.done } : t)
    case 'DELETE':
      return state.filter(t => t.id !== action.id)
    default:
      return state
  }
}

function TodoList() {
  const [todos, dispatch] = useReducer(todoReducer, [])

  return (
    <div>
      <button onClick={() => dispatch({ type: 'ADD_TODO', text: '新任务' })}>
        添加
      </button>
      {todos.map(todo => (
        <div key={todo.id}>
          <span style={{ textDecoration: todo.done ? 'line-through' : 'none' }}>
            {todo.text}
          </span>
          <button onClick={() => dispatch({ type: 'TOGGLE', id: todo.id })}>
            {todo.done ? '撤销' : '完成'}
          </button>
        </div>
      ))}
    </div>
  )
}

有什么好处?所有状态变更逻辑集中在 todoReducer 里,组件只管触发 dispatch。测试时你不需要渲染组件,直接测 reducer 函数的输入输出即可。

React Hooks vs Vue Composables

React HooksVue Composables
执行时机每次 re-render 都跑一遍只在 setup 时跑一次
依赖追踪手动声明依赖数组自动追踪(Proxy)
闭包问题常见,需小心处理不存在(ref 是引用)
心智负担较高(规则、闭包、memo)较低
复用模式自定义 Hook可组合函数

🎯 React Hooks 的「重渲染即执行」模型决定了你必须时刻注意闭包和依赖数组。Vue 的响应式系统(Proxy + ref)天然避免了这些问题——ref 是引用,无论什么时候读 .value 拿到的都是最新值。两种方案没有绝对的优劣,但写 React 时确实需要更警觉。

Hooks 使用规则速查

  1. 只在顶层调用 — 不在条件、循环、return 之后调用 Hook
  2. 只在 React 函数中调用 — 函数组件或自定义 Hook 中,不在普通 JS 函数
  3. 自定义 Hook 以 use 开头 — 让 React 和 ESLint 能识别
  4. 依赖数组别撒谎 — 用了什么外部变量就写进去,或用 ESLint 的 react-hooks/exhaustive-deps 规则帮你检查

掌握了这些模式,你已经能驾驭 90% 的 React 日常开发了。记住:Hooks 让你用更少的代码做更多的事,但前提是你理解它的运行机制,而不是死记硬背规则。

🔗相关忍术

00:00:00
系统已稳定运行:
Nuxt 3Vue 3Tailwind 3
粤ICP备2026111544号
🍥木葉忍村影分身之术!在代码、设计与写作间穿梭的技术忍者。这里记录着技...

© 2025-2026 木葉忍村 · Powered by Nuxt 3 & Tailwind CSS