永远不要欺骗 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。
相关推荐
咪库咪库咪14 分钟前
Vue3-生命周期
前端
莪_幻尘41 分钟前
你的 AI Skill 越多越蠢?Token 上下文爆炸的求生指南
前端·ai编程
lichenyang4531 小时前
从 has.echo 到异步 API 注册表:一次 ASCF API 回调不触发的排查复盘
前端
林瞅瞅1 小时前
Nuxt3 项目部署 Nginx 防盗链后特定 JS 文件 403 问题修复方案
前端
kyriewen2 小时前
别再每次都 Google 了:我整理了前端日常最常踩的 10 个 Git 坑,附速查表
前端·javascript·git
一颗奇趣蛋2 小时前
Web 视频开发完全指南:从入门到精通
前端
非洲农业不发达2 小时前
windows终端体验大升级,让你拥有macos级别的美化
前端·后端
妙码生花2 小时前
从 PHP 到 AI + Golang,程序员自救转型手记(十七):登录接口完善,登录页接口整合,解决跨域
前端·后端·ai编程
唐诗2 小时前
改 3 行配置,我的 Tauri dev 冷启动从 100 秒干到 4 秒
前端·客户端
SmartBoyW3 小时前
深入ECMAScript规范:彻底搞懂JS隐式类型转换与底层ToPrimitive机制
前端·javascript