You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

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;

问题分析

你的顾虑完全成立,严格模式下确实会出现这种情况:

  1. 第一次useEffect执行,触发refreshAccessToken,initialized设为true
  2. React立即执行清理函数,componentIsMounted变为false
  3. 如果此时请求刚好完成,finally里的setDisplayPage(true)会因为componentIsMounted为false被跳过
  4. 第二次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

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.08.02 19:41:05