永远不要欺骗 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。
相关推荐
崔庆才丨静觅6 小时前
hCaptcha 验证码图像识别 API 对接教程
前端
passerby60617 小时前
完成前端时间处理的另一块版图
前端·github·web components
掘了7 小时前
「2025 年终总结」在所有失去的人中,我最怀念我自己
前端·后端·年终总结
崔庆才丨静觅7 小时前
实用免费的 Short URL 短链接 API 对接说明
前端
崔庆才丨静觅7 小时前
5分钟快速搭建 AI 平台并用它赚钱!
前端
崔庆才丨静觅8 小时前
比官方便宜一半以上!Midjourney API 申请及使用
前端
Moment8 小时前
富文本编辑器在 AI 时代为什么这么受欢迎
前端·javascript·后端
崔庆才丨静觅8 小时前
刷屏全网的“nano-banana”API接入指南!0.1元/张量产高清创意图,开发者必藏
前端
剪刀石头布啊8 小时前
jwt介绍
前端
爱敲代码的小鱼8 小时前
AJAX(异步交互的技术来实现从服务端中获取数据):
前端·javascript·ajax