React18中useEffect重复执行、useCallback无效如何解决
React 18 版本中,开发环境下的<React.StrictMode>默认开启了组件首次挂载的重挂载检查:为了提前定位effect缺少清理逻辑导致的潜在问题,会自动执行「组件挂载 -> 模拟卸载 -> 组件重新挂载」的流程,因此没有配置清理函数的useEffect会被连续执行两次。
这个行为仅在开发环境生效,生产环境构建后不会触发重复挂载逻辑,useEffect只会正常执行一次。该问题和你是否使用useCallback包裹请求方法无关,useCallback本身也无法阻止Strict Mode下的effect重执行。
方案1:为请求增加取消逻辑(官方推荐)
这是最符合React设计思路的方案,既可以解决开发环境重复请求的问题,也能避免组件卸载后请求仍在运行导致的内存泄漏、状态更新报错问题,生产环境同样适用。核心实现是借助AbortController在effect的清理函数中取消上一次未完成的请求,修改后的代码如下:
import { useEffect, useCallback } from "react"; function App() { const fetchUsers = useCallback(async (signal) => { try { const response = await fetch( "https://jsonplaceholder.typicode.com/users", { signal } ); const data = await response.json(); console.log("data: ", data); } catch (error) { // 过滤主动取消请求抛出的异常 if (error.name !== "AbortError") { console.log("Error in catch!", error); } } }, []); useEffect(() => { const abortController = new AbortController(); fetchUsers(abortController.signal); // effect重执行/组件卸载前,取消上一次未完成的请求 return () => abortController.abort(); }, [fetchUsers]); } export default App;
该方案下第一次挂载触发的请求会在重挂载流程中被主动取消,只有第二次发起的请求会正常返回并处理结果,不会出现控制台重复打印数据的问题。
方案2:开发环境增加单次执行标记
如果你的请求逻辑暂时无法接入取消能力,可以借助useRef做开发环境下的执行标记,拦截重复的请求调用。注意不要依赖该标记实现业务幂等,它仅用于兼容开发环境的Strict Mode检查,生产环境下组件不会触发重挂载流程,该标记不会影响正常的请求发起逻辑:
import { useEffect, useCallback, useRef } from "react"; function App() { // 标记请求是否已经发起过 const hasFetchedRef = useRef(false); const fetchUsers = useCallback(async () => { try { const response = await fetch( "https://jsonplaceholder.typicode.com/users" ); const data = await response.json(); console.log("data: ", data); } catch (error) { console.log("Error in catch!"); } }, []); useEffect(() => { if (hasFetchedRef.current) return; hasFetchedRef.current = true; fetchUsers(); }, [fetchUsers]); } export default App;
不建议通过移除
<React.StrictMode>的方式规避问题。Strict Mode触发的重复执行本质是提前暴露代码隐患:如果你的effect没有写清理逻辑,实际生产环境中组件正常的重挂载、路由跳转场景下,一样会出现重复请求、内存泄漏、重复事件绑定等问题,Strict Mode只是把这些问题提前在开发阶段暴露出来,降低线上故障概率。
内容的提问来源于stack exchange,提问作者ram

