永远不要欺骗 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。
相关推荐
●VON18 小时前
React Native for OpenHarmony:2048 小游戏的开发与跨平台适配实践
javascript·学习·react native·react.js·von
木斯佳18 小时前
前端八股文面经大全:26届秋招滴滴校招前端一面面经-事件循环题解析
前端·状态模式
光影少年19 小时前
react状态管理都有哪些及优缺点和应用场景
前端·react.js·前端框架
saber_andlibert20 小时前
TCMalloc底层实现
java·前端·网络
逍遥德20 小时前
如何学编程之01.理论篇.如何通过阅读代码来提高自己的编程能力?
前端·后端·程序人生·重构·软件构建·代码规范
冻感糕人~20 小时前
【珍藏必备】ReAct框架实战指南:从零开始构建AI智能体,让大模型学会思考与行动
java·前端·人工智能·react.js·大模型·就业·大模型学习
程序员agions20 小时前
2026年,“配置工程师“终于死绝了
前端·程序人生
alice--小文子20 小时前
cursor-mcp工具使用
java·服务器·前端
晚霞的不甘21 小时前
揭秘 CANN 内存管理:如何让大模型在小设备上“轻装上阵”?
前端·数据库·经验分享·flutter·3d
小迷糊的学习记录21 小时前
0.1 + 0.2 不等于 0.3
前端·javascript·面试