跳到主要内容

React useEffect闭包陷阱问题

面试速答

  • 以下内容以 React 18 为准。
  • useEffect 的闭包陷阱,本质上不是 Hook 神秘,而是 JavaScript 闭包拿到的是某次 render 的变量快照
  • 如果 effect 里的回调长期存在,但依赖没更新,就会一直读到旧值。
  • 常见解决方案有 4 个:
    • 依赖写全
    • 用函数式更新
    • useRef 存最新值
    • 拆分 effect,缩小副作用职责

典型场景

1. 定时器读到旧值

useEffect(() => {
const id = setInterval(() => {
console.log(count)
}, 1000)

return () => clearInterval(id)
}, [])

这里打印的往往一直是初始 count,因为定时器回调闭包绑定的是第一次 render 的值。

2. 事件监听拿到旧状态

useEffect(() => {
function onScroll() {
console.log(keyword)
}

window.addEventListener('scroll', onScroll)
return () => window.removeEventListener('scroll', onScroll)
}, [])

如果 keyword 后续变了,监听器里的值还是旧的。

为什么会这样

函数组件每次 render 都会创建新的作用域。effect 回调和它内部创建的函数,捕获的是“那一轮 render 的变量”。

所以问题不是 React 算错了,而是你让一个长期存在的副作用,继续引用了旧快照。

常见解决方案

1. 依赖写全

useEffect(() => {
const id = setInterval(() => {
console.log(count)
}, 1000)

return () => clearInterval(id)
}, [count])

代价是每次 count 变化都要重建副作用。

2. 用函数式更新

如果只是“基于旧值继续算”,可以直接:

setCount(c => c + 1)

这能绕开很多旧值问题。

3. 用 useRef 保存最新值

const latestCount = useRef(count)
latestCount.current = count

长期存在的回调里读 latestCount.current,就能拿到最新值。

4. 拆 effect

不要把订阅、请求、派生状态、日志都塞进同一个 effect,否则依赖关系会越来越难控制。

React 18 下要补的一句

开发环境 StrictMode 会额外执行一轮 effect 挂载和清理,但它只是让问题更容易暴露,不是闭包陷阱的根因。

典型题标准答法

问:useEffect 闭包陷阱的本质是什么?

答:effect 和回调捕获的是某一次 render 的状态快照,如果副作用长时间存在而依赖没更新,就会持续使用旧值。

易错点

  • 依赖数组故意少写,结果以为自己“优化了性能”。
  • StrictMode 双执行误认为闭包问题的根源。
  • 所有问题都用 useRef 硬顶,导致响应式关系不清晰。

速记要点

  • 闭包拿到的是 render 快照。
  • 长期副作用最容易读旧值。
  • 优先保证依赖正确,其次再谈优化。