😱 React `<StrictMode>`:为什么 useEffect 被调用了两次?别慌,这是特性不是 bug

问题场景

你是不是也遇到过这种情况:项目里把根组件包上 <StrictMode> 之后,开发环境下 useEffect 竟然执行了两次,接口请求发了两次、console.log 打印了两遍、定时器被创建了两个......

第一反应往往是:卧槽,是不是我写了什么 bug?然后开始疯狂排查依赖数组、清理函数,甚至怀疑是不是 React 内部出问题了。结果一查才发现,这是 React 故意设计的开发期行为

更让人困惑的是:为什么有的 useEffect 会双调用,有的不会?为什么生产环境一切正常?

今天就把这个「React 开发期专属迷惑行为」彻底讲清楚,并给出正确应对姿势。

原因分析

1. StrictMode 到底做了什么?

<StrictMode> 是 React 提供的一个开发环境专用的检测工具,它本身不渲染任何 UI,只在开发模式下开启额外的检查:

  • 组件函数会执行两次(模拟「卸载再挂载」)
  • useState / useMemo / useReducer 的初始化函数会被调用两次
  • useEffect / useLayoutEffect 会经历「挂载 → 清理 → 再挂载」的完整循环

生产环境下 <StrictMode> 的一切检查都会被完全关闭,这也是为什么你「切到线上就没问题了」。

2. 为什么 React 要这么干?

核心目的就一个:帮你暴露「不纯」的副作用代码

React 的哲学是「渲染应当是纯函数」------同样的输入,应该得到同样的输出。可现实中很多开发者会把副作用写进渲染过程,或者写出「忘记清理」的 effect。StrictMode 通过双调用,逼着你在开发期就暴露这些问题:

  • 如果你在 effect 里忘记清理订阅/定时器/事件监听 → 双调用后会出现重复注册、内存泄漏 → 立刻暴露
  • 如果你在渲染函数体里直接改了外部状态 → 双调用会暴露数据被污染

一句话:它在开发期故意「制造麻烦」,让你在生产期少踩坑。

3. 为什么有的 effect 只执行一次?

StrictMode 只在组件首次挂载 时进行双调用模拟。如果某个组件是在后续更新阶段才挂载的(比如条件渲染后才出现),那这次挂载是「正常挂载」,只有一次。这也是很多人「时灵时不灵」的原因。

解决方案(含实操代码)

✅ 正确认知:这不是 bug,不用「修」

首先稳住心态。看见 useEffect 双执行,先别急着改代码。大多数情况下你什么都不用做,只要保证你的 effect 是「可清理、可重复执行」的即可。

✅ 规范写法:永远提供清理函数

这是应对 StrictMode 的根本解法。让 effect 幂等、可逆:

tsx 复制代码
// ❌ 错误:只订阅不清理
useEffect(() => {
  socket.on('message', handler);
}, []);

// ✅ 正确:成对出现,提供 cleanup
useEffect(() => {
  socket.on('message', handler);
  return () => {
    socket.off('message', handler); // 清理,保证可重挂载
  };
}, []);

定时器同理:

tsx 复制代码
useEffect(() => {
  const timer = setInterval(tick, 1000);
  return () => clearInterval(timer); // 关键!
}, []);

只要清理函数写得够干净,StrictMode 的双调用对你就是透明的:第一次挂载被你清掉了,第二次干净地重新挂载,行为一致。

✅ 接口请求发了两次?两种思路

思路一(推荐):升级到「数据获取库」或让请求自己幂等。 不要每次 mount 都发请求,用 React Query / SWR 这类库,它们自带缓存 + 去重,天然免疫 StrictMode 双请求:

tsx 复制代码
import useSWR from 'swr';

function User() {
  // SWR 自动去重,并发相同 key 只发一次请求
  const { data } = useSWR('/api/user', fetcher);
  return <div>{data?.name}</div>;
}

思路二:手动加锁变量(治标,不推荐新代码使用,但老代码兼容可救急):

tsx 复制代码
useEffect(() => {
  let cancelled = false;

  async function load() {
    const res = await fetch('/api/user');
    if (!cancelled) {
      setUser(res.data); // 只有没被取消才更新状态
    }
  }
  load();

  return () => {
    cancelled = true; // 第一次挂载的清理:标记取消
  };
}, []);

注意这里的关键点:StrictMode 的第二次挂载是真正的挂载 ,所以请求还是会发,只是用 cancelled 挡住了第一次的「过期状态更新」。如果要彻底只发一次请求,还是得上缓存层(思路一)。

✅ 我确认代码没问题,就是不想看它双执行?

可以只保留 mode="production" 下关闭,但开发期不建议 关闭 StrictMode------它会帮你提前暴露真实 bug。如果是某个特定的第三方组件 在双调用下有已知问题,也可以只在它外层用 unstable_StrictMode(不推荐)或临时注释。

正确姿势:保留 StrictMode,把自家代码改干净。它是最好的「副作用体检仪」。

✅ 检查「双重执行」还可能是这些情况

如果排除了 StrictMode,useEffect 仍然异常多次执行,请按顺序排查:

  1. 依赖数组里的引用类型每次 render 都变(对象/数组字面量)→ 导致每次渲染都触发
  2. 父组件在每次 render 都重建,子组件随之频繁挂载
  3. 组件没有 key 或 key 不稳定,导致 React 反复销毁重建
tsx 复制代码
// 经典坑:依赖数组里放了字面量对象 → 每次都触发
useEffect(() => {
  doSomething();
}, [{ id }]); // ❌ 每次 render 都是新对象引用

// 正确:依赖原始值
useEffect(() => {
  doSomething();
}, [id]); // ✅

要点总结

  • <StrictMode>开发期专用 的副作用「体检仪」,会让 useEffect 经历「挂载 → 清理 → 再挂载」,生产环境完全无效
  • 不是 bug,别费劲想「关掉它」,而是把代码写规范。
  • 应对核心:给 effect 写干净的 cleanup 函数,让副作用幂等、可重挂载。
  • 接口双请求:优先用 SWR / React Query 这类带缓存去重的库,天然免疫。
  • 别忘了排查真正的问题来源:依赖数组里的引用类型 、不稳定的 key,这些才是会「污染」生产环境的元凶。
  • 一句话方法论:StrictMode 是朋友,它双执行是为了让你在生产环境少踩一次坑。 顺着它把代码改对,比关掉它更省心。

以后看到 useEffect 跑两遍,先别骂框架------它是为了你好。👍

相关推荐
Asize1 小时前
2 道大厂面试题:TS 工具类型我懂了,CSS 3 列布局把我问住了
前端·css·typescript
胡萝卜术1 小时前
从"氛围编程"到规范驱动:两次创造如何让 AI 协作从碰运气变成工程流水线
前端·面试·github
Imchendiana1 小时前
《狂人日记NO.11》— 给 AI 装一本"项目说明书":我把"自己"蒸馏成了一个编码知识库Skill
前端·ai编程
l1258651 小时前
# RAG多轮对话检索设计:Query重写如何让“那它呢“变成完整问题
前端·数据库·人工智能·python·算法·fastapi·milvus
এ慕ོ冬℘゜1 小时前
前端实战:使用 jQuery 与 CSS3 打造动态数据表格与滑块交互
前端·css3·jquery
Csvn1 小时前
✂️ AbortController:一个 API 统一取消 fetch、事件监听与 AI 流式请求
前端
invicinble1 小时前
对于vue2转vue3相关技术整合
前端
MacroZheng2 小时前
腾讯又开源了一个新项目,用起来真优雅!
前端·vue.js·人工智能