useEffect 依赖数组怎么用?
依赖数组不是“控制执行次数”的开关,而是 effect 对响应式数据的声明
先记住核心规则:effect 使用了某个会随渲染变化的值,就应该让它出现在依赖数组中;React 会在 render 阶段逐项比较新旧依赖值,并在提交更新后执行需要重新同步的 effect。
本文只讲依赖数组。关于 effect 和布局 effect 的区别,可以阅读useEffect vs useLayoutEffect。
1. 三种写法
空数组:[]
含义是:这个 effect 不依赖后续渲染中的响应式值。它会在首次提交后执行 setup,组件卸载时执行 cleanup,后续重新渲染不会因为依赖变化而重新执行。
典型场景包括初始化只依赖常量的第三方实例、订阅固定的外部资源等。不要把它机械理解成类组件的 componentDidMount;开发环境 Strict Mode 可能额外执行一次 setup/cleanup 检查,代码必须能够安全地重复执行。
不传依赖数组:useEffect(setup)
每次组件提交更新后都可能执行。它通常不是首选,因为任何 state 或父组件更新都可能触发这个 effect。只有确实需要响应每次提交时才使用,并确保 cleanup 正确。
传入依赖数组:[count, userId]
effect 会在首次提交后执行;之后 React 逐项比较依赖值,只有至少一个依赖发生变化时,才会先执行旧 cleanup,再执行新的 setup。
依赖值使用 Object.is 比较,而不是深度比较:
所以对象、数组和函数只要引用变化,就会被视为依赖变化。
2. render 阶段和 commit 阶段分别做什么?
useEffect 的 setup 不会在 render 阶段执行。组件函数执行到 useEffect 时,React 主要是在当前 Fiber 上登记这个 effect,保存 setup、dependencies 等信息:
可以把一次更新理解成:
新的 dependencies 来自当前这次 render 执行 useEffect 时传入的第二个参数;旧的 dependencies 来自上一次已经提交的 current Fiber 中保存的 Hook/effect 记录。React 在构建 workInProgress Fiber 时,可以通过对应的 current Fiber 找到上一次的 Hook,从而取出旧 dependencies。
如果某次 render 被中断或丢弃,它产生的 dependencies 不会成为下一次比较的旧值。只有成功 commit 的那棵 Fiber 树,才会变成后续更新中的 current Fiber。
因此更准确地说:render 阶段负责登记 effect 并比较依赖,commit 阶段负责提交更新,useEffect 的 setup 会在 commit 后被调度执行。
3. 什么应该放进依赖数组?
应该放入 effect 中使用的响应式值,例如:
- 组件的 props;
useState或useReducer返回的 state;- 在组件函数体内创建的对象、数组和函数;
- 从 context 中读取的值。
不要为了让 lint 通过而随意删除依赖。eslint-plugin-react-hooks 的 exhaustive-deps 规则通常能帮助发现闭包和同步问题。
有些值不需要作为依赖:
- setter(如
setCount)的引用由 React 保证稳定; useRef返回的 ref 对象引用稳定;- 模块级常量和组件外部定义的稳定函数。
但 ref.current 的变化不会触发渲染,不能依赖它来驱动 effect 重新执行。如果需要响应某个值的变化,应把它放进 state 或 props。
4. 闭包陷阱
每次渲染都是一次独立的函数调用,effect 捕获的是创建它的那次渲染中的变量:
这个定时器捕获的是首次渲染的 count,因此后续打印的仍可能是 0。这不是 React 没有更新 state,而是 effect 没有重新创建,闭包仍然使用旧快照。
解决方式一:加入依赖
每次 count 变化都会重建定时器。逻辑正确,但如果订阅或初始化成本较高,可能需要进一步重构。
解决方式二:只更新状态时使用函数式更新
updater 会接收 React 当前处理到的状态,因此不需要从闭包读取 count。
解决方式三:确实需要读取最新值时使用 ref
ref 适合保存不需要触发渲染的最新值,但它会绕开 React 的响应式数据流,不应作为逃避依赖数组的默认方案。
如果使用的是支持 useEffectEvent 的 React 版本,也可以把“需要读取最新值、但不希望它触发 effect 重新同步”的逻辑拆出去。Effect Event 本身不应该放进依赖数组:
这里访问 url 是响应式逻辑,url 变化就应该重新记录访问;读取 shoppingCart.length 只是记录当时的最新信息,不希望购物车变化也触发一次访问记录。
5. 对象、数组和函数依赖怎么办?
下面的 options 每次父组件渲染都会创建新对象:
即使内容相同,options 引用也不同,effect 仍会重新执行。
优先按下面顺序处理:
方案一:在 effect 内创建只供 effect 使用的对象
这样依赖从对象引用变成了真正影响 effect 的原始值。
方案二:依赖具体字段
仅当 effect 确实只使用 color 时才能这样写;如果使用了整个对象,就不能只填一个字段。
方案三:确实需要稳定引用时使用 useMemo 或 useCallback
缓存是性能优化手段,不是让依赖数组“看起来不变”的万能修复。应先确认重复执行确实造成了问题,再引入缓存。
6. 依赖变化时 cleanup 的顺序
当依赖发生变化时,React 处理顺序可以理解为:
这保证了切换 roomId 时旧连接会被清理,避免同时保留多个订阅。cleanup 也会在组件卸载时执行。开发环境 Strict Mode 的额外 setup/cleanup 是为了验证这段逻辑是否健壮。
7. 异步请求和竞态问题
effect 中发请求时,依赖数组只能保证“什么时候发起新请求”,不能自动保证“只有最新请求能更新状态”:
如果 userId 从 1 很快切到 2,旧请求可能比新请求更晚返回。cleanup 中把旧 effect 的 ignore 标记为 true,可以避免旧结果覆盖新结果。
如果底层 API 支持取消请求,也可以使用 AbortController:
面试里可以这样总结:依赖变化时旧 cleanup 会先执行,所以 cleanup 不只用于释放订阅,也常用于取消或忽略过期异步任务。
8. 为什么 effect 会无限循环?
无限循环通常同时满足两个条件:
- effect 里更新了 state。
- 这个 state 更新导致某个依赖变化,从而再次触发 effect。
这里每次 setOptions({ keyword }) 都会创建新对象,options 引用变化后又触发 effect,于是循环继续。修复方式不是删除依赖,而是先判断这个 state 是否真的需要存在:
如果只是根据 props 或 state 派生数据,通常应该在渲染阶段直接计算;如果计算很重,可以用 useMemo;如果确实要同步外部系统,再放进 effect。
9. 事件逻辑不要塞进 effect
用户操作直接导致的逻辑,优先写在事件处理器里,而不是通过 effect 观察某个状态再补做:
更直接的写法是:
判断标准是:如果这段逻辑是“组件出现在屏幕上或某些响应式值变化后,需要和外部系统保持同步”,它适合 effect;如果它是“用户刚刚做了某个动作,所以要执行”,它更适合事件处理器。
10. SSR 和客户端限制
useEffect 只会在客户端执行,服务端渲染阶段不会执行。因此依赖 effect 才能得到的数据,首屏 HTML 中通常没有这部分结果:
在 SSR 或 Next.js 场景下,面试官可能会继续问:首屏数据是否应该在服务端获取?是否会造成 hydration 后内容变化?浏览器 API 是否只能放在客户端 effect 中?这些问题的核心仍然是区分“渲染所需数据”和“客户端外部系统同步”。
11. 依赖数组不是执行次数开关
以下写法经常是信号:effect 里的逻辑和依赖设计需要重新思考:
如果确实要在首次提交时使用初始值,应明确说明这是业务意图,并考虑把初始值保存到 ref;如果 effect 应随 userId 变化,就应该写 [userId]。
另一个常见问题是用 effect 派生本可直接计算的数据:
只有与外部系统同步时才需要 effect,例如网络、浏览器 API、订阅、定时器或第三方组件。
总结
最终记住这几点:
- 依赖数组逐项使用
Object.is比较,不是深度比较。 - 新 dependencies 来自本次 render,旧 dependencies 来自上一次已提交的 current Fiber。
- render 阶段登记 effect 并比较依赖,
useEffect的 setup 在 commit 后执行。 - effect 使用的响应式值要诚实声明。
- 每次渲染都有自己的闭包,函数式更新可以避免读取旧 state。
- cleanup 会在下一次 setup 前执行,也会在卸载时执行。
- 异步 effect 要处理过期结果或取消请求。
- 无限循环通常来自 effect 更新 state 后又改变依赖。
- 事件触发的逻辑优先放事件处理器,不要绕到 effect。
useEffect不在服务端执行,不适合作为首屏服务端数据来源。- 不要用 effect 派生普通数据,也不要把依赖数组仅当作执行次数开关。

