写 React 时,你有没有遇到过「定时器里的 state 永远不更新」的诡异情况?比如明明写了setCount(count + 1),页面上的count却永远停在 1------ 这其实是 ** 闭包陷阱(Stale Closure)** 在搞鬼。
今天用一个极简示例,拆解这个坑的本质,再给你 2 个一劳永逸的解决方案。
一、先看复现:count 为什么永远停在 1?
先看这段 "看似没问题" 的代码:

运行结果 :页面上的count从 0 变成 1 后,就再也不涨了。
二、核心原因:闭包 "定格" 了初始 state
问题出在 2 个关键点的叠加:
- useEffect 的空依赖
[]:空依赖意味着useEffect只在组件挂载时执行 1 次,后续组件更新不会重新运行这个 effect。 - 闭包捕获了 "快照" 值 :
useEffect执行时,内部的setInterval函数形成了闭包 ------ 它 "抓住" 了当时的count(值为 0)。后续count虽然被更新,但因为useEffect没重新执行,这个闭包永远拿着初始值 0 ,所以setCount(count + 1)永远是0 + 1 = 1。
三、2 个解决方案:从根源避开闭包陷阱
针对这个场景,推荐 2 种既简单又安全的写法:
方案 1:函数式更新(推荐)
用setState的函数式写法,直接获取最新的 state 值,绕开闭包的旧值:

原理 :setCount(c => c + 1)会从 React 内部获取当前最新的count值,不管闭包抓的是旧值,都能拿到最新数据。
方案 2:补全依赖数组
把count加入useEffect的依赖数组,让useEffect在count变化时重新执行,生成新的闭包:

注意 :这个方案会频繁创建 / 清理定时器 (每次count变化都重新执行 effect),性能不如方案 1,仅推荐在 "必须依赖外部变量" 的场景使用。
四、避坑总结:useEffect + 定时器的正确姿势
- 优先用函数式更新 :
setState(prev => prev + 1)是避开闭包陷阱的 "万能钥匙"; - 空依赖要谨慎 :空依赖的
useEffect里,尽量避免直接引用 state/props,改用函数式更新; - 依赖数组要写全 :如果必须依赖外部变量,一定要把变量加入依赖数组(配合 ESLint 的
react-hooks/exhaustive-deps规则)。