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

React Router双路由守卫竞态异常:useEffect与useNavigate失效

路由守卫竞态条件导致登录跳转异常问题解决

核心问题分析

你的问题本质是路由跳转时机与守卫状态更新的竞态冲突:

  • 登录成功后调用navigate('/projects'),路由立即切换触发LocalGuard渲染,但GlobalGuard中的异步pingUser请求还未完成,validUser仍为false
  • LocalGuard拿到false的上下文后直接跳回/login,后续GlobalGuard完成请求更新validUser时,路由已经切回登录页,状态更新无法触发正确跳转

解决方案

方案1:合并守卫逻辑,避免嵌套竞态

直接在GlobalGuard内统一处理权限判断与路由跳转,移除LocalGuard减少状态传递延迟:

修改GlobalGuard代码:

const dispatch = useDispatch();
const [loading, setLoading] = useState(true);
const location = useLocation();
const navigate = useNavigate();

useEffect(() => {
  (async () => {
    try {
      const token = localStorage.getItem('token');
      const localUser = token ? JSON.parse(localStorage.getItem('user')) : null;
      
      if (token && localUser?.email) {
        const pingStatus = await pingUser({ email: localUser.email });
        if (pingStatus === 200) {
          dispatch(fillUser(localUser));
          // 登录成功后直接跳转项目页
          if (location.pathname === '/login') {
            navigate('/projects');
          }
        } else {
          // 验证失败,清空存储并跳转登录
          localStorage.removeItem('user');
          localStorage.removeItem('token');
          if (location.pathname !== '/login') {
            navigate('/login');
          }
        }
      } else {
        // 无有效凭证,跳转登录(排除登录页本身)
        if (location.pathname !== '/login') {
          navigate('/login');
        }
      }
    } catch (err) {
      localStorage.removeItem('user');
      localStorage.removeItem('token');
      if (location.pathname !== '/login') {
        navigate('/login');
      }
    } finally {
      setLoading(false);
    }
  })();
}, [location.pathname, navigate, dispatch]);

return loading ? <ServerLoad /> : <Outlet />;

简化路由配置:

<Router>
  <Routes>
    <Route element={<GlobalGuard />}>
      <Route path="/login" element={<Login />} />
      <Route path="/projects/*" element={<Projects />}>
        ...
      </Route>
    </Route>
  </Routes>
</Router>

方案2:让LocalGuard等待GlobalGuard加载完成

如果必须保留LocalGuard,需让它等待GlobalGuard的异步操作结束后再做权限判断:

修改GlobalGuard,将loading和validUser同时传入上下文:

// GlobalGuard返回部分
return loading ? <ServerLoad /> : <Outlet context={{ loading, validUser }} />;

修改LocalGuard代码:

const { loading, validUser } = useOutletContext();
const navigate = useNavigate();

// 等待GlobalGuard验证完成
if (loading) {
  return <ServerLoad />;
}

return validUser ? <Outlet /> : <Navigate to="/login" />;

方案3:登录后直接更新全局状态

登录成功后直接在login.jsx中标记用户有效性,避免依赖守卫的异步请求:

修改login.jsx的onSubmit:

const onSubmit = async (data) => {
  try {
    const res = await loginUser(data);
    dispatch(setUser(res));
    localStorage.setItem('token', res.token);
    localStorage.setItem('user', JSON.stringify(res.user));
    // 直接标记用户有效
    dispatch(setValidUser(true));
    navigate('/projects');
  } catch (err) {
    // 处理登录错误
  }
};

修改GlobalGuard优先使用全局状态:

const dispatch = useDispatch();
const [loading, setLoading] = useState(true);
const validUserFromStore = useSelector(state => state.user.valid);
const location = useLocation();

useEffect(() => {
  (async () => {
    try {
      // 仅当全局状态未标记有效时,才执行ping验证
      if (!validUserFromStore) {
        const token = localStorage.getItem('token');
        const localUser = token ? JSON.parse(localStorage.getItem('user')) : null;
        
        if (token && localUser?.email) {
          const pingStatus = await pingUser({ email: localUser.email });
          if (pingStatus === 200) {
            dispatch(fillUser(localUser));
            dispatch(setValidUser(true));
          } else {
            localStorage.removeItem('user');
            localStorage.removeItem('token');
            dispatch(setValidUser(false));
          }
        } else {
          dispatch(setValidUser(false));
        }
      }
    } catch (err) {
      dispatch(setValidUser(false));
    } finally {
      setLoading(false);
    }
  })();
}, [location.pathname, dispatch, validUserFromStore]);

return loading ? <ServerLoad /> : <Outlet context={validUserFromStore} />;

关键注意点

  • 尽量让最外层守卫统一处理权限逻辑,避免嵌套守卫的状态传递延迟
  • 登录成功后优先更新全局状态,减少对localStorage和异步请求的依赖
  • 所有守卫逻辑必须等待异步操作完成后再进行路由判断,避免提前跳转

内容的提问来源于stack exchange,提问作者mr.loop

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.07.02 09:43:14