DEBUG.WIKI
ZgoCloud:多地区部署,按需选节点;日本、美国、香港、德国
雨云;给项目一台云服务器
RackNerd;$21.99/年 · 1GB KVM

React StrictMode 下,useEffect 为什么先 setup、cleanup,再 setup

StrictMode 开发检查会重跑 Effect。按每轮资源配对清理,区分重渲染、重复请求及服务端写入,不用 ref 跳过第二次 setup。

React 18 升级后,开发环境里的请求、订阅或初始化日志出现两次,生产构建却没有同样的重跑。若应用根节点启用了 StrictMode,先看 Effect 的 setup 和 cleanup 是否成对,不要直接加一个 ref 把第二次执行挡住。

这里讨论 React 18 起根节点 StrictMode 的开发检查。组件函数重渲染、Effect 重跑和实际网络请求数量是不同的观察值;不能只凭两行 console 日志判断请求发送了两次,也不能把生产环境的重复提交都归因于 StrictMode。

两次 setup 中间应该有清理

React 在开发检查中会额外安排一次 setup、cleanup,再进行实际使用的 setup。Effect 依赖变化时,也会先清理上一轮,再建立下一轮;cleanup 不只在组件最终离开页面时调用。

下面是浏览器 React 组件的完整示例。它订阅窗口尺寸,清理时移除自己注册的同一个函数:

import { useEffect, useState } from 'react';

export default function WindowWidth() {
  const [width, setWidth] = useState(null);

  useEffect(() => {
    const updateWidth = () => setWidth(window.innerWidth);
    updateWidth();
    window.addEventListener('resize', updateWidth);
    return () => window.removeEventListener('resize', updateWidth);
  }, []);

  return <p>窗口宽度:{width ?? '读取中'}</p>;
}

重跑后应只留下当前这一轮的监听器。定时器对应 clearInterval,连接对应关闭连接,外部控件对应销毁实例;应清理本轮创建的资源,而不是清空整个应用的所有订阅。

如果用 useRef(false) 标记“已经执行过”,第一次 cleanup 已移除了资源,第二次 setup 又被跳过,页面反而可能没有有效订阅。关掉 StrictMode 可以辅助比较环境,但不能证明资源生命周期已经修好。

请求取消和忽略旧结果,不等于撤销服务端操作

读取数据的 Effect 需要考虑组件离开或参数切换后的旧结果。可以使用每轮 Effect 独立的 ignore 变量阻止旧响应更新状态,或者用请求库支持的取消机制。不要用一个跨所有 Effect 的共享 ref 混淆不同轮次。

取消浏览器请求不保证服务器尚未接收、更不保证服务器撤回已经执行的写入。下单、付款、删除等由用户操作触发的动作,应放在对应事件流程中,并由服务端根据业务要求处理重复提交;不能把这些操作塞进挂载 Effect,再指望开发检查只执行一次。

已使用框架的数据加载或查询缓存时,优先检查其缓存、取消和去重语义,不要再套一层手写 Effect。缓存让并发读取复用结果,也不表示 React 组件只渲染一次。

保留 StrictMode,反复挂载、离开页面和切换依赖后检查资源数量及旧响应:旧监听器已移除,上一轮结果不会覆盖新状态,当前资源仍可用。生产环境仍重复时,另查组件是否真实重挂载、依赖是否每次改变,以及事件是否被重复注册。