← 中文资源

简体中文版本

React — StrictMode 下开发环境 Effect 执行两次

理解 React StrictMode 在开发环境为何双重执行 Effect,订阅与请求为何重复,以及如何让清理完全对称。

阅读英文原文

症状

  控制台用三行讲完故事:subscribe、cleanup、subscribe——这正是 StrictMode 在履行职责:挂载、卸载、再挂载,让缺失的清理当场现形。React 18 开发环境下这是刻意行为,生产不受影响;“开发重复、生产正常”的不对称正是它的指纹。对每个被重复执行的 Effect,诊断问题只有一个:我的清理是否完整撤销了设置?缺 AbortController、没清的 interval、无补偿逻辑的统计上报,都会在这里最先暴露。

诊断思路

  先在 Effect 与 cleanup 里各打一行日志确认“设置-清理-设置”序列;再审计所有 useEffect 的清理对称性——有副作用的 Effect 必须有完全撤销它的 return,没有 return 的就是重复候选;最后按幂等性分类副作用:统计事件、支付意图这类非幂等操作要移到事件处理器按逻辑动作触发,而不是随 Effect 执行。

根因

  React 18 StrictMode 在开发环境对每个组件做一次重挂载,以验证 Effect 能自我清理。这是有意设计:健壮的应用应能在挂载-卸载-挂载循环中幸存,双重执行暴露的正是清理没有完全逆转设置的 Effect。

修复与验证

  把对称当铁律:设置做什么,清理就撤销什么——订阅配退订、定时器配清除、fetch 配 abort(并单独捕获 AbortError 不让它污染错误监控)。非幂等动作归属用户意图:checkout 点击时 track 一次,而不是挂载时。验证时保持 StrictMode 开启:监听器计数稳定无重复订阅、网络面板显示一条 in-flight 被中止后再一条完成、统计按逻辑动作恰好一次。关掉 StrictMode 只是让症状静音——路由切换与 Suspense 重挂载在生产会触发同样的 bug。

代码示例

subscribe            <- effect body 1
cleanup              <- StrictMode unmount
subscribe            <- effect body 2
useEffect(() => {
  const controller = new AbortController();
  fetch(url, { signal: controller.signal })
    .then(r => r.json())
    .then(setData)
    .catch(e => { if (e.name !== 'AbortError') setError(e); });
  return () => controller.abort();   // fully undoes the setup
}, [url]);

useEffect(() => {
  const handler = (e) => setPos(e);
  window.addEventListener('pointermove', handler);
  return () => window.removeEventListener('pointermove', handler);
}, []);
function onCheckout() {
  track('checkout_started', { cart });   // once per click, not per mount
  createPaymentIntent(cart);
}