React 18严格模式下App重复执行触发多次服务端调用问题
问题背景
测试React函数调用逻辑时,发现在严格模式下App.js内的函数会被多次调用,导致文件重复执行、触发多次服务端请求,当前使用的React版本为18。
相关代码
index.js
import React from 'react'; import ReactDOM from 'react-dom/client'; import './index.css'; import App from './App'; import reportWebVitals from './reportWebVitals'; const root = ReactDOM.createRoot(document.getElementById('root')); root.render( <React.StrictMode> <App /> </React.StrictMode> ); reportWebVitals();
App.js
import React from "react"; import "./App.css"; import Auth from "./Auth"; import { Route, Routes, BrowserRouter } from "react-router-dom"; function App() { return ( <BrowserRouter> <Routes> <Route path="/" element={<Auth />} /> </Routes> </BrowserRouter> ); } export default App;
Auth.js
import React, { useState, useRef } from "react"; import "./App.css"; import CheckCurrentUser from "./CheckCurrentUser"; let counter = 0; let currentState; function Auth() { const [loginState, setLoginState] = useState(false); const CurrentAuthenticatedUser = useRef(undefined); const loading = useRef(false); async function checkLoginState() { if (!loading.current) { console.log(loading.current); loading.current = true; console.log(loading.current); counter += 1; console.log("before the await function : " + counter); if (!CurrentAuthenticatedUser.current) { console.log("counter value inside the if is :" + counter); console.log( "current authenticated user is :" + CurrentAuthenticatedUser.current ); CurrentAuthenticatedUser.current = await CheckCurrentUser(); console.log(loading.current); setLoginState(CurrentAuthenticatedUser.current); loading.current = false; } } console.log("after the await function : " + counter); } function loadingCheckLoginState() { checkLoginState(); loading.current = true; } console.log("executing before"); currentState = loading.current ? console.log("loading is on") : loadingCheckLoginState(); console.log("executing after"); return <h1>Open Points</h1>; } export default Auth;
CheckCurrentUser.js
function CheckCurrentUser() { try { console.log("inside check current user document try block"); fetch(`https://jsonplaceholder.typicode.com/posts`).then((response) => console.log(response) ); return true; } catch (error) { console.log("no logged in user - inside catch block"); return null; } } export default CheckCurrentUser;
核心疑问
- 为什么App.js会被执行两次?
- 为何组件第二次执行时,会忽略前一次执行生成的状态结果?
问题解答
1. App.js执行两次的核心原因
这是React 18严格模式(React.StrictMode)下仅开发环境生效的默认检测行为,生产环境打包后不会出现。
React 18为了提前揪出代码里不符合并发渲染规则的问题,会在开发环境自动执行双挂载流程:组件首次挂载完成后立刻触发一次卸载,再重新挂载一次组件。整个过程不会真的销毁重建DOM,专门用来检测组件有没有副作用没清理、逻辑不幂等(执行多次和执行一次结果不一致)这类隐藏bug。
你在index.js里把根组件包裹在了<React.StrictMode>标签中,所以App组件以及它所有的子组件都会触发这个双调用检测。
2. 第二次执行读不到前一次状态的原因
要分两类存储的变量来看:
- React托管的状态:用
useState、useRef创建的状态和ref值,都是和组件实例绑定的。严格模式下第一次挂载后卸载组件时,React会直接销毁这一轮实例对应的所有状态、ref值,第二次挂载是全新的组件实例,所有hook都会重新走初始化流程,自然读不到上一轮存的值。 - 组件外部声明的普通全局变量:比如你代码里写的
counter、currentState,这类变量不归React管,两次组件执行操作的都是内存里同一个变量,所以你会看到counter的值一直在累加。你写的loadingref拦截逻辑之所以没挡住重复请求,就是因为第一次挂载给ref设的true值,在组件卸载时被重置成了初始的false,第二次挂载判断时还是会进请求逻辑。
额外注意:你现在把请求逻辑直接写在组件渲染函数顶层执行,本身就不符合React的开发规范。副作用类逻辑(比如接口请求、事件绑定)必须放到
useEffect里执行,同时写好卸载时的清理逻辑,这样不管是严格模式双调用,还是后续并发渲染的调度,都不会出现重复触发请求的问题。另外你的CheckCurrentUser函数里fetch没有加await,函数返回true的时候请求还没完成,返回值和实际请求结果完全脱节,这个逻辑也需要修正。
内容的提问来源于stack exchange,提问作者Gourab Paul
相关产品推荐
相关产品推荐

