React Hooks 踩坑合集,全是血泪教训

用了两年 React,Hooks 的坑踩了个遍。整理一下最常遇到的几个,都是代码里真实出过的问题。

useState 的更新不是同步的

jsx 复制代码
function Counter() {
  const [count, setCount] = useState(0)

  const handleClick = () => {
    setCount(count + 1)
    setCount(count + 1)
    setCount(count + 1)
    // count 最终是 1,不是 3
  }
}

三次 setCount(count + 1) 用的都是同一个 count 值(0),所以结果都是 1。React 会把同一轮渲染中的多次 setState 合并(batching)。

正确写法:用函数式更新

jsx 复制代码
const handleClick = () => {
  setCount(prev => prev + 1)
  setCount(prev => prev + 1)
  setCount(prev => prev + 1)
  // 这次是 3
}

useEffect 的依赖数组别骗自己

jsx 复制代码
function UserProfile({ userId }) {
  const [user, setUser] = useState(null)

  useEffect(() => {
    fetchUser(userId).then(setUser)
  }, []) // 依赖数组漏了 userId!

  return <div>{user?.name}</div>
}

依赖数组写了 [],意思是"只在挂载时执行一次"。但 userId 变了的时候,effect 不会重新跑,页面显示的还是第一个用户的信息。

ESLint 的 exhaustive-deps 规则一定要开,它会帮你检查依赖数组是否完整。

jsx 复制代码
useEffect(() => {
  fetchUser(userId).then(setUser)
}, [userId]) // 正确:userId 变了重新拉取

如果确实只想跑一次(比如埋点上报),那就老老实实写 [],但要在注释里说明为什么。

useRef 不触发重渲染

这个不算坑,但很多人刚用的时候会困惑:

jsx 复制代码
function Timer() {
  const [count, setCount] = useState(0)
  const renderCount = useRef(0)

  renderCount.current += 1
  console.log('render:', renderCount.current) // 能正确打印 1, 2, 3...

  return <button onClick={() => setCount(c => c + 1)}>{count}</button>
}

useRef.current 变了,但页面不会重新渲染。它就是一个不会触发更新的"变量容器"。适合存不需要反映到 UI 上的值(定时器 ID、DOM 引用、上一次的 props 等)。

useMemo 不是免费的

很多人觉得 useMemo 是性能优化万能药,什么都包一层:

jsx 复制代码
// 过度使用
const name = useMemo(() => user.firstName + ' ' + user.lastName, [user])
const isAdmin = useMemo(() => user.role === 'admin', [user])
const items = useMemo(() => list.filter(x => x.active), [list])

useMemo 本身有开销------要存依赖数组、做浅比较。如果计算本身很轻量(字符串拼接、简单判断),useMemo 的开销可能比直接算还大。

什么时候该用

  • 计算确实很重(大数组排序/过滤、复杂 JSON 转换)
  • 作为其他 Hook 的依赖(useEffect 的依赖项需要引用稳定)
  • 传给子组件的对象/数组(避免子组件不必要的重渲染)

什么时候不该用

  • 简单运算、字符串拼接
  • 组件本身就很轻量
  • 没有传给子组件,也没有作为依赖

useCallback 和 React.memo 要配套

单独用 useCallback 没什么意义:

jsx 复制代码
// 没有意义
function Parent() {
  const handleClick = useCallback(() => {
    console.log('clicked')
  }, [])

  return <Child onClick={handleClick} />
}

// Child 没有 memo,每次 Parent 渲染 Child 都会重渲染
function Child({ onClick }) {
  console.log('Child render')
  return <button onClick={onClick}>Click</button>
}

useCallback 缓存了函数引用,但 Child 没有用 React.memo 包裹,所以不管 onClick 变没变,Child 都会重渲染。

jsx 复制代码
// 配套使用
const Child = React.memo(function Child({ onClick }) {
  console.log('Child render')
  return <button onClick={onClick}>Click</button>
})

React.memo 做浅比较,useCallback 保证引用不变 → 浅比较通过 → 子组件跳过渲染。

自定义 Hook 复用逻辑

写了三遍类似的"请求 + loading + error"之后,就该抽成自定义 Hook 了:

jsx 复制代码
function useFetch(url) {
  const [data, setData] = useState(null)
  const [loading, setLoading] = useState(true)
  const [error, setError] = useState(null)

  useEffect(() => {
    let cancelled = false
    setLoading(true)

    fetch(url)
      .then(res => res.json())
      .then(data => {
        if (!cancelled) {
          setData(data)
          setLoading(false)
        }
      })
      .catch(err => {
        if (!cancelled) {
          setError(err.message)
          setLoading(false)
        }
      })

    return () => { cancelled = true } // 组件卸载时取消
  }, [url])

  return { data, loading, error }
}

// 使用
function UserList() {
  const { data, loading, error } = useFetch('/api/users')

  if (loading) return <Spinner />
  if (error) return <div>Error: {error}</div>
  return <ul>{data.map(u => <li key={u.id}>{u.name}</li>)}</ul>
}

cancelled 标记是为了防止组件卸载后还 setState(会报警告)。

总结

Hooks 的核心心智模型是"每次渲染都是一次独立的函数调用"。理解了这一点,大部分坑都能自己推理出来:useState 的批量更新、useEffect 的闭包问题、useCallback 的引用缓存,都是这个模型的直接推论。

相关推荐
触底反弹7 小时前
🔥 从执行上下文到 React 陷阱——彻底搞懂 JavaScript 闭包
javascript·react.js
李明卫杭州1 天前
React 复合事件系统对比解析
前端·javascript·react.js
白雾茫茫丶1 天前
Vibecoding 一个主题切换动画库:13 种揭幕方式
前端·vue.js·react.js
西瓜太郎4991 天前
别再手算 Token 成本:用 React 状态机做一个可追溯的模型费用计算器
react.js·typescript
右耳朵猫AI1 天前
Web前端周刊2026W37 | Shopify 转原生、React 编译 Rust 化、Vitest 5.0、Rslib 1.0
前端·javascript·react.js·typescript·node.js
csj501 天前
前端基础之《React(13)—表单绑定、列表渲染》
前端·react.js
csj501 天前
前端基础之《React(12)—条件渲染》
前端·react.js
晚安日记wanna2 天前
SSR 为什么不适合登录态从水合冲突到缓存串号
前端·react.js·面试
liangshanbo12153 天前
React useEffect 与 useLayoutEffect 面试题整理
前端·javascript·react.js