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 快照。
- 长期副作用最容易读旧值。
- 优先保证依赖正确,其次再谈优化。