问题现象
在 React 18 的开发环境中,你可能会发现组件的 useEffect(以及 useLayoutEffect、useMemo、useReducer 的初始化函数等)会被连续执行两次。例如:
useEffect(() => {
console.log('Effect 执行');
return () => {
console.log('Effect 清理');
};
}, []);
控制台输出:
Effect 执行
Effect 清理
Effect 执行
如果该 effect 中包含了数据请求,你可能看到两次请求发出,进而导致数据重复获取或其他不可预期的行为。这常常让刚接触 React 18 的开发者感到困惑,甚至误认为是 Bug。
根本原因:React 18 的严格模式(Strict Mode)
React 从 v18 开始,在开发环境下的 <React.StrictMode> 中会故意重复调用某些函数,包括:
- 函数组件体(整个函数)
useState、useMemo、useReducer的初始化函数useEffect、useLayoutEffect、useInsertionEffect的回调函数- 这些 effects 的清理函数也会被执行一次,然后重新执行 effect
目的只有一个:帮助开发者暴露潜在的错误。
具体来说,React 在开发环境下会模拟组件的挂载 → 卸载 → 重新挂载这一过程。也就是说,你的组件会被立即卸载,然后马上再挂载一次。这种“双重调用”是为了检查你的 effect 是否正确地处理了清理逻辑,能否在快速连续的挂载/卸载过程中保持正确行为。
为什么需要这样做
React 在未来会引入一项功能:当用户从页面 A 切到页面 B,再从页面 B 切回页面 A 时,React 可以选择保留页面 A 的状态并在后台复用其组件,即所谓的“Offscreen”或“可复用状态”模式。为了让组件能够安全地在“不可见”和“重新可见”之间切换,每个 effect 必须能正确清理并重新设置。
StrictMode 的双重调用就是一种预演:如果 effect 没有正确清理副作用(比如没有移除事件监听、没有清除定时器),那么当组件被“卸载又挂载”后,就会出现内存泄漏、双重绑定等问题。通过强制执行两次,React 让你在开发阶段就能发现这些问题,而不是在用户实际使用时才暴露。
正确处理方式
核心原则:每个 effect 都应该是“可重复执行”且“可安全清理”的。
- 始终提供清理函数
如果 effect 里创建了任何需要手动清理的产物(订阅、定时器、事件监听、网络取消等),必须在清理函数中彻底销毁。
useEffect(() => {
const timer = setInterval(() => {
console.log('tick');
}, 1000);
// 清理函数:必须清除定时器
return () => clearInterval(timer);
}, []);
- 数据请求需要具备可取消或忽略陈旧结果的能力
如果 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)这样的数据请求库,它们内置了请求去重和缓存,可以天然规避重复请求问题。
- 避免在非浏览器环境无效的副作用
部分副作用只应在浏览器环境运行,如果直接在 effect 里操作 DOM,但没有检查 SSR 环境,可能导致服务端渲染报错。双重调用同样会暴露这类问题,促使开发者使用 useEffect(仅在客户端执行)而不是直接在渲染期间操作 DOM。
特殊情况:是否需要关闭 StrictMode?
<React.StrictMode> 包裹了你的应用(通常由 CRA 或 Vite 模板默认配置)。有的开发者可能想通过删除 <StrictMode> 来消除“双重执行”。但这只是掩盖问题,而非解决问题。
除非你正在维护一个无法重构的旧项目,且双重调用导致严重的第三方库兼容问题(极少见),否则强烈建议保留严格模式。它能帮助你编写更健壮、为未来 React 特性做好准备的代码。
生产环境的表现
StrictMode 的双重调用仅发生在开发环境。在生产构建中,组件只会正常地挂载一次,effect 也只执行一次。所以你不需要担心这个特性会影响线上性能或行为。
总结
- 现象:开发环境 useEffect 执行两次。
- 原因:React 18 的 StrictMode 模拟组件挂载 → 卸载 → 重新挂载,探测不正确的副作用处理。
- 处理:为每个 effect 编写可靠的清理逻辑,数据请求需可取消,确保组件在连续挂卸载过程中不会产生内存泄漏或错误状态。
- 建议:保留 StrictMode,不要关闭它,把双重执行当作代码质量的“压力测试”。
当你习惯了这种思想后,会发现 useEffect 的“双重执行”并不是一个烦恼,而是一个帮你提前排雷的贴心设计。