永远不要欺骗 React:详解 useEffect 依赖规则与“闭包陷阱”

如果你在 useEffect 内部使用了某个 prop 或 state,但没有把它放到依赖数组里,你会遇到 React 中最著名的 Bug ------ 闭包陷阱 (Stale Closure)

这意味着:你的 Effect 只能"看见"旧的数据,永远看不见新的数据。


1. 为什么"必须"放?(原理演示)

看这个经典的错误例子:

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

  useEffect(() => {
    const id = setInterval(() => {
      console.log(count); // ❌ 永远打印 0
    }, 1000);
    return () => clearInterval(id);
  }, []); // ⚠️ 空数组:意思是"只在挂载时运行一次"
}

发生了什么?

  1. 第一次渲染 (Mount): count 是 0。useEffect 执行,创建了一个定时器。这个定时器捕获了当时count (也就是 0)。
  2. 第二次渲染: 用户点了按钮,count 变成了 1。
  3. React 检查依赖: 依赖数组是 [] (空的),跟上次一样。
  4. React 决定: "既然依赖没变,那我就不重新运行 Effect 了。"
  5. 结果: 旧的定时器还在跑,它手里的 count 依然是第一次渲染时的那个 0 。它永远不知道外面 count 已经变了。

这就是"对 React 撒谎"的代价。


2. 但是,如果你把它们放进去...

JavaScript 复制代码
useEffect(() => {
  const id = setInterval(() => {
    console.log(count);
  }, 1000);
  return () => clearInterval(id);
}, [count]); // ✅ 加进去了

现在的行为:

  1. count 变了 (0 -> 1)。
  2. React 发现依赖变了。
  3. 清除旧的定时器。
  4. 运行 新的 Effect,创建新的定时器(捕获新的 count 1)。

问题来了: 定时器被不断重置,这可能不是你想要的(比如会导致计时不准)。


3. 如何"既不撒谎,又不重置"?

可以使用 useRefuseEffectEvent。这两个工具存在的意义,就是为了合法地把变量从依赖数组里拿出来。

方法 A:使用函数式更新 (如果是 setState)

如果你只是想修改状态,不需要读取它:

JavaScript 复制代码
useEffect(() => {
  const id = setInterval(() => {
    // ✅ 不需要依赖 count,因为 prev 永远是 React 传给你的最新值
    setCount(prev => prev + 1); 
  }, 1000);
}, []); // ✅ 空数组是安全的

方法 B:使用 useRef (逃生舱)

如果你需要读取值,但不想触发 Effect 重跑:

JavaScript 复制代码
const countRef = useRef(count);
// 每次渲染都同步最新值
useEffect(() => { countRef.current = count });

useEffect(() => {
  const id = setInterval(() => {
    // ✅ 读 ref,永远是最新的,且 ref 不需要放进依赖
    console.log(countRef.current); 
  }, 1000);
}, []); // ✅ 安全

方法 C:使用 useEffectEvent (最新标准)

我们在上一个问题里用到的方法:

JavaScript 复制代码
const onTick = useEffectEvent(() => {
  console.log(count); // ✅ 在这里读最新值
});

useEffect(() => {
  const id = setInterval(() => {
    onTick();
  }, 1000);
}, []); // ✅ 安全

总结

  1. 官方规则 (ESLint): 凡是用到的响应式数据(props, state, context),必须 全部填入依赖数组。不要试图通过欺骗 linter (// eslint-disable) 来解决逻辑问题。

  2. 后果: 如果不填,代码会引用旧值(闭包陷阱)。

  3. 正确做法: 如果你不希望某个变量导致 Effect 重新运行,不要简单地把它从数组里删掉,而是应该:

    • useRef 把它包起来。
    • 或者用 useEffectEvent 把它隔离开。
    • 或者检查是否可以移出 Effect。
相关推荐
eason_fan1 小时前
从一则内存快照看iframe泄漏:活跃与Detached状态的回收差异
前端·性能优化
狗头大军之江苏分军1 小时前
年底科技大考:2025 中国前端工程师的 AI 辅助工具实战盘点
java·前端·后端
编程修仙2 小时前
第三篇 Vue路由
前端·javascript·vue.js
比老马还六2 小时前
Bipes项目二次开发/硬件编程-设备连接(七)
前端·javascript
掘金一周2 小时前
前端一行代码生成数千页PDF,dompdf.js新增分页功能| 掘金一周 12.25
前端·javascript·后端
张就是我1065922 小时前
漏洞复现指南:利用 phpinfo() 绕过 HttpOnly Cookie 保护
前端
Kagol2 小时前
🎉TinyVue v3.27.0 正式发布:增加 Space 新组件,ColorPicker 组件支持线性渐变
前端·vue.js·typescript
潍坊老登2 小时前
大前端框架汇总/产品交互参考UE
前端
方安乐3 小时前
获取URL参数如何避免XSS攻击
前端·xss
十二AI编程3 小时前
MiniMax M2.1 实测,多语言编程能力表现出色!
前端