React函数组件useEffect首次渲染时API重复调用两次解决方法
你遇到的API重复调用、日志重复打印问题,是React 18版本后**开发环境默认开启的严格模式(Strict Mode)**的预期行为,不是你写的路由或者组件逻辑有bug。
严格模式下,组件首次挂载流程中React会主动执行一次「挂载→卸载→重新挂载」的操作,目的是提前检测组件副作用的写法问题,比如忘记清理的定时器、事件监听、未做中断的重复请求等隐患,生产环境打包构建后不会出现这个双调用现象。
方案1:直接关闭严格模式(仅适合临时调试,不推荐长期使用)
找到项目入口文件(通常是main.jsx/index.js),移除根组件外层的<React.StrictMode>包裹即可:// 修改前 root.render( <React.StrictMode> <App /> </React.StrictMode> ); // 修改后 root.render(<App />);这个方案会丢失严格模式的代码隐患检测能力,长期维护项目不建议这么做。
方案2:给useEffect增加清理逻辑,主动中断重复请求(官方推荐标准写法)
严格模式下第一次执行effect后,会立刻触发effect的return清理函数,再重新执行effect。你只要在清理函数里加上请求中断逻辑,就能避免无效的重复请求落库、重复处理返回值的问题,同时还能兼容生产环境下组件快速卸载、依赖变更重跑effect的场景,避免内存泄漏。
修改后的User组件代码示例(基于axios + AbortController实现):import React, { useEffect, useState } from "react"; import axios from "axios"; // 你自己封装的Api.Get方法也可以扩展支持signal参数 // import Api from "./api"; export default function User() { const [userInfo, setUserInfo] = useState(null); useEffect(() => { const abortController = new AbortController(); const getUser = async () => { try { // 给请求传入中断信号 const res = await axios.get("/User", { signal: abortController.signal }); // 如果你用自己封装的Api.Get,可以改成下面的写法 // const res = await Api.Get("User", { signal: abortController.signal }); if (res.status === 200) { console.log(res); setUserInfo(res.data); } } catch (err) { // 主动中断的请求会抛出异常,直接过滤即可 if (axios.isCancel(err) || err.name === "AbortError") return; console.error("请求异常:", err); } }; getUser(); console.log(111); // 清理函数:effect重跑/组件卸载前中断未完成的请求 return () => { abortController.abort(); }; }, []); return ( <> <div>Hello</div> {userInfo && <div>当前用户:{userInfo.name}</div>} </> ); }方案3:使用成熟的请求库自动处理
如果项目里用SWR、TanStack Query(原React Query)这类数据请求库,它们内部已经默认做了开发环境重复请求去重、请求缓存、无效请求中断的逻辑,直接按照库的文档写请求即可,不需要手动处理严格模式的双调用问题。
避坑提醒:不要用
useRef存挂载标记位(比如判断if(!didMount.current)才发请求)的方式规避双调用,这种写法会绕过严格模式的检测,后续如果组件真的因为业务逻辑需要卸载重挂,会出现请求不触发的逻辑bug。
内容的提问来源于stack exchange,提问作者Jack The Baker

