React应用中API调用被重复多次触发该如何解决?
问题诱因
你遇到的重复打印、重复触发请求的问题,核心原因分两类:
- React 18 开发环境严格模式(Strict Mode)的默认校验行为:这是最主要的诱因。React 18 版本起,开发环境下严格模式会主动执行两次组件挂载、卸载的流程,用来提前检测副作用不规范、内存泄漏等代码问题。这个双重执行逻辑只会在开发环境生效,生产环境打包后不会出现。
- 代码本身的写法问题放大了重复执行的表现:你把
console.log(response)直接写在组件函数顶层,组件每次重渲染、重挂载都会执行这行代码;同时你的请求逻辑没有做防重、没有做清理,副作用触发时就会重复发起请求、重复更新state,进一步触发更多次重渲染。
另外你的代码里还有一个容易被忽略的语法错误:fetch请求配置的请求头字段名是headers(带s),你写的是header,会导致请求头不生效,可能引发接口返回异常。
你提供的单次点击按钮后的控制台输出截图如下:
修复方案
不建议直接关闭严格模式
严格模式的双重校验是React官方提供的代码质量检测能力,能帮你提前定位很多隐蔽问题,为了消掉重复日志关掉严格模式属于因小失大。修正请求逻辑,增加防重和清理机制
不管是按钮触发请求还是初始化自动拉取,都要做重复请求拦截,useEffect里的请求要加取消逻辑,避免组件重执行时触发无效请求:修正后的按钮触发请求代码:
import { useState, useRef, useEffect } from 'react'; function App() { const [ response , setResponse ] = useState(); const [ btnText , setbtnText ] = useState('Get Data'); const [ details , setDetails ] = useState(); // 用ref做请求锁,防止重复点击触发重复请求 const requestLock = useRef(false); async function fetchData() { if (requestLock.current) return; requestLock.current = true; setbtnText('Loading...'); try { // 修正headers字段名,统一async/await写法避免链式调用冗余 const res1 = await fetch('url hidden' , { method: 'POST', headers: {'Content-Type': 'application/json'}, }); const data1 = await res1.json(); setResponse(data1); const res2 = await fetch('url hidden' , { method: 'POST', headers: {'Content-Type': 'application/json'}, }); const data2 = await res2.json(); setDetails(data2); } catch (error) { console.log(error); } finally { requestLock.current = false; setbtnText('Get Data'); }; } // 不要在组件顶层直接打日志,放到useEffect里监听对应值变化再打印,避免无意义的日志输出 useEffect(() => { console.log(response); }, [response]) return ( <div className="container"> <header className='header'> <button onClick={fetchData} disabled={requestLock.current}>{btnText}</button> </header> <Summary response={response} details={details} /> </div> ); }
修正后的初始化自动拉取(useEffect版本)代码,增加请求取消逻辑,适配严格模式的双重执行:
useEffect(() => { // 用AbortController做请求取消,避免重复请求和组件卸载后更新state的警告 const controller = new AbortController(); const signal = controller.signal; const fetchInitData = async () => { try { const [res1Data, res2Data] = await Promise.all([ fetch('url hidden' , { method: 'POST', headers: {'Content-Type': 'application/json'}, signal }).then(res => res.json()), fetch('url hidden' , { method: 'POST', headers: {'Content-Type': 'application/json'}, signal }).then(res => res.json()), ]); // 确认请求没被取消再更新state if (!signal.aborted) { setResponse(res1Data); setDetails(res2Data); } } catch(err) { // 主动取消的请求不需要打错误日志 if (err.name !== 'AbortError') { console.log(err); } } } fetchInitData(); // effect重执行/组件卸载时取消上一次未完成的请求 return () => controller.abort(); } , [])
- 用网络面板判断真实请求次数
不要靠控制台的state日志判断请求是否重复,打开浏览器开发者工具的「网络」面板,查看实际的接口请求记录:加了上述清理逻辑后,严格模式下触发的多余请求会被标记为已取消,不会真的打到你的DynamoDB后端,生产环境构建后只会存在单次有效请求。
内容的提问来源于stack exchange,提问作者Baslki
相关产品推荐
相关产品推荐

