人人都会AI编程

29.1 useEffect 执行两次的原因与处理

更新时间:2026-07-11

问题现象

在 React 18 的开发环境中,你可能会发现组件的 useEffect(以及 useLayoutEffectuseMemouseReducer 的初始化函数等)会被连续执行两次。例如:

useEffect(() => {
  console.log('Effect 执行');
  return () => {
    console.log('Effect 清理');
  };
}, []);

控制台输出:

Effect 执行
Effect 清理
Effect 执行

如果该 effect 中包含了数据请求,你可能看到两次请求发出,进而导致数据重复获取或其他不可预期的行为。这常常让刚接触 React 18 的开发者感到困惑,甚至误认为是 Bug。

根本原因:React 18 的严格模式(Strict Mode)

React 从 v18 开始,在开发环境下的 <React.StrictMode> 中会故意重复调用某些函数,包括:

  • 函数组件体(整个函数)
  • useStateuseMemouseReducer 的初始化函数
  • useEffectuseLayoutEffectuseInsertionEffect 的回调函数
  • 这些 effects 的清理函数也会被执行一次,然后重新执行 effect

目的只有一个:帮助开发者暴露潜在的错误。

具体来说,React 在开发环境下会模拟组件的挂载 → 卸载 → 重新挂载这一过程。也就是说,你的组件会被立即卸载,然后马上再挂载一次。这种“双重调用”是为了检查你的 effect 是否正确地处理了清理逻辑,能否在快速连续的挂载/卸载过程中保持正确行为。

为什么需要这样做

React 在未来会引入一项功能:当用户从页面 A 切到页面 B,再从页面 B 切回页面 A 时,React 可以选择保留页面 A 的状态并在后台复用其组件,即所谓的“Offscreen”或“可复用状态”模式。为了让组件能够安全地在“不可见”和“重新可见”之间切换,每个 effect 必须能正确清理并重新设置。

StrictMode 的双重调用就是一种预演:如果 effect 没有正确清理副作用(比如没有移除事件监听、没有清除定时器),那么当组件被“卸载又挂载”后,就会出现内存泄漏、双重绑定等问题。通过强制执行两次,React 让你在开发阶段就能发现这些问题,而不是在用户实际使用时才暴露。

正确处理方式

核心原则:每个 effect 都应该是“可重复执行”且“可安全清理”的

  1. 始终提供清理函数

如果 effect 里创建了任何需要手动清理的产物(订阅、定时器、事件监听、网络取消等),必须在清理函数中彻底销毁。

useEffect(() => {
  const timer = setInterval(() => {
    console.log('tick');
  }, 1000);

  // 清理函数:必须清除定时器
  return () => clearInterval(timer);
}, []);
  1. 数据请求需要具备可取消或忽略陈旧结果的能力

如果 effect 里发起了一个 fetch 请求,当组件被卸载或依赖变化时,应该丢弃过时的响应,避免在组件已经卸载后执行 setState 导致内存泄漏警告。

useEffect(() => {
  let ignore = false;

  async function fetchData() {
    const result = await fetch('/api/data');
    if (!ignore) {
      setData(await result.json());
    }
  }

  fetchData();
  return () => {
    ignore = true;
  };
}, []);

在 React 18 的开发环境下,组件会被立即卸载再挂载,你的请求会被发出两次。第一次请求的结果会被 ignore 忽略,第二次请求的结果会正确设置状态。虽然网络请求仍然发了两次(开发环境无法避免),但逻辑不会出错。

更推荐的做法是使用像 TanStack Query(React Query)这样的数据请求库,它们内置了请求去重和缓存,可以天然规避重复请求问题。

  1. 避免在非浏览器环境无效的副作用

部分副作用只应在浏览器环境运行,如果直接在 effect 里操作 DOM,但没有检查 SSR 环境,可能导致服务端渲染报错。双重调用同样会暴露这类问题,促使开发者使用 useEffect(仅在客户端执行)而不是直接在渲染期间操作 DOM。

特殊情况:是否需要关闭 StrictMode?

<React.StrictMode> 包裹了你的应用(通常由 CRA 或 Vite 模板默认配置)。有的开发者可能想通过删除 <StrictMode> 来消除“双重执行”。但这只是掩盖问题,而非解决问题。

除非你正在维护一个无法重构的旧项目,且双重调用导致严重的第三方库兼容问题(极少见),否则强烈建议保留严格模式。它能帮助你编写更健壮、为未来 React 特性做好准备的代码。

生产环境的表现

StrictMode 的双重调用仅发生在开发环境。在生产构建中,组件只会正常地挂载一次,effect 也只执行一次。所以你不需要担心这个特性会影响线上性能或行为。

总结

  • 现象:开发环境 useEffect 执行两次。
  • 原因:React 18 的 StrictMode 模拟组件挂载 → 卸载 → 重新挂载,探测不正确的副作用处理。
  • 处理:为每个 effect 编写可靠的清理逻辑,数据请求需可取消,确保组件在连续挂卸载过程中不会产生内存泄漏或错误状态。
  • 建议:保留 StrictMode,不要关闭它,把双重执行当作代码质量的“压力测试”。

当你习惯了这种思想后,会发现 useEffect 的“双重执行”并不是一个烦恼,而是一个帮你提前排雷的贴心设计。