React中useEffect执行两次引发的竞态条件问题排查
React严格模式下useEffect竞态条件与路由守卫的问题处理
问题场景与代码
这是一个用于路由保护的React组件,页面刷新时会从sessionStorage恢复认证状态,调用refreshAccessToken更新Redux中的token。为了避免React严格模式开发环境下重复调用refreshAccessToken导致后端token失效,使用了顶层变量initialized控制执行次数,但担心存在竞态条件:严格模式下useEffect会执行两次,第一次触发请求后,清理函数将componentIsMounted设为false,如果此时请求刚好完成,finally里的setDisplayPage(true)会被跳过;第二次执行useEffect时,因为initialized已经是true,不会再触发请求,导致页面一直显示LOADING...
原代码如下:
// Variables declared top-level outside the component let initialized = false; let componentIsMounted = true; const PrivateRouteGuard = () => { const token = useSelector((state) => state?.auth?.token); const userIsAuthenticated = Boolean(token) || sessionStorage.getItem("isAuthenticated"); const [displayPage, setDisplayPage] = useState(false); const dispatch = useDispatch(); useEffect(() => { componentIsMounted = true; if (!initialized) { initialized = true; console.log("✅ Only runs once per app load"); if (!token && userIsAuthenticated) { refreshAccessToken() .then((response) => { // Add the new Access token to redux store dispatch(addAuthToken({ token: response?.data?.accessToken })); return getUserProfile(); // Get authenticated user using token in redux store }) .then((response) => { const user = response.data?.user; // Add authenticated user to redux store dispatch(addAuthUser({ user })); }) .finally(() => { componentIsMounted && setDisplayPage(true); }); } else { setDisplayPage(true); } } return () => componentIsMounted = false; }, []); if (!displayPage) { return "LOADING..."; // Display loading indicator here } if (!userIsAuthenticated) { return ( <Navigate to="/login" /> ); } return <Outlet />; }; export default PrivateRouteGuard;
问题分析
你的顾虑完全成立,严格模式下确实会出现这种情况:
- 第一次useEffect执行,触发
refreshAccessToken,initialized设为true - React立即执行清理函数,
componentIsMounted变为false - 如果此时请求刚好完成,
finally里的setDisplayPage(true)会因为componentIsMounted为false被跳过 - 第二次useEffect执行,因为
initialized已经是true,不会进入请求逻辑,也不会调用setDisplayPage(true),导致页面一直停在LOADING状态
另外,顶层变量initialized和componentIsMounted是全局的,当多个PrivateRouteGuard实例存在时会引发更严重的状态混乱。
解决方案
1. 用组件内的Ref替代顶层变量
使用useRef保存组件级别的状态,避免全局变量的污染,同时在严格模式下能正确追踪组件实例的挂载状态。
2. 调整请求逻辑的执行时机
确保在组件第二次挂载时,即使第一次的请求已经完成,也能正确更新displayPage状态;或者直接在第二次挂载时判断是否需要重新处理状态。
优化后的代码:
import { useState, useEffect, useRef } from 'react'; import { useSelector, useDispatch } from 'react-redux'; import { Navigate, Outlet } from 'react-router-dom'; import { addAuthToken, addAuthUser } from './authSlice'; // 假设的slice路径 const PrivateRouteGuard = () => { const token = useSelector((state) => state?.auth?.token); const userIsAuthenticated = Boolean(token) || sessionStorage.getItem("isAuthenticated"); const [displayPage, setDisplayPage] = useState(false); const dispatch = useDispatch(); // 用ref保存组件实例的初始化状态和挂载状态 const initializedRef = useRef(false); const isMountedRef = useRef(true); useEffect(() => { isMountedRef.current = true; // 只有当未初始化且需要刷新token时才执行请求 if (!initializedRef.current && !token && userIsAuthenticated) { initializedRef.current = true; console.log("✅ Only runs once per component instance"); refreshAccessToken() .then((response) => { dispatch(addAuthToken({ token: response?.data?.accessToken })); return getUserProfile(); }) .then((response) => { const user = response.data?.user; dispatch(addAuthUser({ user })); }) .finally(() => { if (isMountedRef.current) { setDisplayPage(true); } }); } else { // 不需要请求时直接设置displayPage setDisplayPage(true); } return () => { isMountedRef.current = false; }; }, [token, userIsAuthenticated, dispatch]); // 添加依赖,确保状态变化时能重新执行 if (!displayPage) { return "LOADING..."; } if (!userIsAuthenticated) { return <Navigate to="/login" />; } return <Outlet />; }; export default PrivateRouteGuard;
关键优化点
- 用
useRef替代全局变量,每个组件实例拥有独立的状态,避免跨实例污染 - 将
token和userIsAuthenticated加入useEffect依赖数组,确保认证状态变化时能正确响应 - 调整逻辑:只有在需要刷新token时才标记
initializedRef.current = true,否则直接设置displayPage(true),避免严格模式下第二次挂载时卡住 - 保留
isMountedRef防止组件卸载后更新状态
额外建议
- 可以考虑将token刷新逻辑封装到Redux thunk中,组件只负责触发动作和处理UI状态,让逻辑更清晰
- 后端可以调整
refreshAccessToken的逻辑,允许短时间内重复调用而不失效,降低前端的处理复杂度
内容的提问来源于stack exchange,提问作者Jane Smitter
相关产品推荐
相关产品推荐

