先记住这个答案
Effect 读取的 props、state,以及组件内根据它们形成的变量和函数,都可能随渲染变化。依赖列表应覆盖这些响应式输入,使 React 在值改变后清理旧同步并建立新同步。遗漏依赖常表现为连接还指向旧对象、定时回调读取旧值或请求使用上一个参数。exhaustive-deps 检查帮助发现这种数据关系缺口,处理警告时应调整真实代码结构,而不是默认关闭规则。
- 依赖由 Effect 实际读取的输入推导
- 闭包保存某轮输入,不能自动变成最新快照
- 不想重新同步时要重构职责,不能隐瞒依赖
漏掉文档 ID 为什么会显示旧订阅结果
假设 Effect 根据 documentId 订阅实时审阅记录,但只声明服务地址作为依赖。文档切换后,组件可以已经显示新标题,旧 Effect 却没有因为 ID 变化而重新建立,于是侧栏仍收到旧文档记录。这个错位来自界面输入和外部同步使用了不同快照。
给依赖补上 documentId,让切换明确触发旧订阅清理和新订阅建立,才恢复了数据关系。若新依赖导致高频重连,还要检查它为何频繁变化、同步范围是否合理;不能因为修正后暴露了额外工作,就再把必要依赖删除。
把独立同步和事件读取分开
一个 Effect 同时管理文档连接与页面背景设置,背景颜色变化就可能连带重建连接。更清楚的结构是把两个外部关系拆开,各自依赖真正需要的输入。拆分不是为了把数组写短,而是让每个过程的重新同步条件与它负责的资源一致。
另一些回调只在连接事件发生时需要读取最新通知偏好,却不要求偏好一变就重新连接。支持 useEffectEvent 的 React 版本可以用它表达这种非响应式的 Effect 事件逻辑,但不能把它作为逃避所有依赖的通道。连接目标等决定同步关系的值仍然必须保留在依赖中,并遵守对应 Hook 的调用限制。
减少依赖应通过证明代码不再读取它
如果定时器只是基于旧计数加一,可以让 setter 接收更新函数,从而不必让定时器闭包读取某个 count 快照。如果配置只在同步内部使用,也可以把配置构造移进 Effect,直接依赖它的业务字段。每种改法都改变了实际读取关系,随后依赖数组才能相应变化。
普通模块常量不属于每次渲染变化的输入,但随意修改的全局变量也不会自动通知 React。组件外部可变数据需要订阅协议,而不是靠依赖数组轮询。对于自定义同步 Hook,还应配置适当的依赖检查,并检查封装是否把调用方真正需要声明的输入隐藏掉了。
容易答错的地方
- 工具不了解业务,直接禁用规则最省事
- 工具确有静态分析边界,但警告经常指出真实的旧闭包问题。应先写清楚为什么某个变化不应重新同步,并让代码表达这个区别,再处理确实无法由工具识别的情况。
- 把全部 state 都加入依赖最安全
- 依赖应对应实际读取,不是整个组件状态清单。无关输入会制造不必要同步,也可能掩盖一个 Effect 混合多个职责的问题;需要完整,但不应随意扩大。
面试官还会怎么问?
setter 为什么经常可以不写进依赖?
React 提供的 state setter 具有稳定身份,依赖检查能够识别某些稳定值,允许省略时可以省略。自定义包装函数或父层传来的函数不能自动套用同样结论。
把最新值放 ref 就一定解决旧闭包吗?
它可以表达某些命令式读取,但也可能绕过必要重同步并增加手动维护。先判断读最新值与因值变化重新连接是否是不同需求,再选择适当机制,避免把所有依赖都藏进 ref。
useEffectEvent 可以在点击事件里调用吗?
它用于 Effect 相关逻辑,不是普通事件处理器的替代品,也不应作为 prop 任意传递。需要用户点击触发的操作应使用正常事件流程,具体约束按项目所用 React 与检查工具版本确认。
参考资料
示例用于理解所注明的运行环境与边界;延伸学习可结合原文中的更多案例。