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

React-Router 6.8.0 路由竞态问题:受保护路由跳转异常求助

解决React受保护路由的竞态问题

你的问题核心是React状态更新的异步特性:setSession触发的上下文状态更新不会同步生效,当signIn函数返回data后立刻执行navigate,受保护路由读取的还是旧的isSignedIn值,因此触发重定向。下面是几种直接可行的解决方案:


方案一:在AuthContext内部处理导航(推荐)

将跳转逻辑移到上下文组件中,利用useEffect监听登录状态变化,确保状态更新完成后再执行导航。

步骤1:修改AuthContext组件

import { useState, useEffect, createContext, useContext } from 'react';
import { useNavigate } from 'react-router-dom';

const AuthContext = createContext();

export function AuthProvider({ children }) {
  const [session, setSession] = useState(null);
  const isSignedIn = !!session;
  const navigate = useNavigate();

  // 监听登录状态变化,自动跳转
  useEffect(() => {
    if (isSignedIn) {
      navigate('/dashboard');
    }
  }, [isSignedIn, navigate]);

  const signIn = async ({ email, password }) => {
    const { data, error } = await client.auth.signInWithPassword({ email, password });
    if (data) setSession(data.user);
    return { error };
  };

  // 其他上下文方法(如signOut)...

  return (
    <AuthContext.Provider value={{ isSignedIn, signIn }}>
      {children}
    </AuthContext.Provider>
  );
}

export const useAuth = () => useContext(AuthContext);

步骤2:简化登录表单的handleSubmit

const handleSubmit = async (e) => {
  e.preventDefault();
  const { error } = await auth.signIn({ email, password });
  if (error) setAuthError(error.message);
};

方案二:让signIn函数等待状态更新完成

通过useRef和useEffect创建一个Promise,确保状态更新后再让signIn函数resolve,这样外部的navigate会在状态更新后执行。

修改AuthContext中的signIn函数

export function AuthProvider({ children }) {
  const [session, setSession] = useState(null);
  const isSignedIn = !!session;
  const signInResolveRef = useRef(null);

  // 监听session变化,触发resolve
  useEffect(() => {
    if (session && signInResolveRef.current) {
      signInResolveRef.current();
      signInResolveRef.current = null;
    }
  }, [session]);

  const signIn = async ({ email, password }) => {
    const { data, error } = await client.auth.signInWithPassword({ email, password });
    if (error) return { data, error };
    
    setSession(data.user);
    // 返回一个Promise,等待状态更新完成
    return new Promise(resolve => {
      signInResolveRef.current = resolve;
    }).then(() => ({ data, error: null }));
  };

  // ...其他代码
}

此时登录表单的handleSubmit无需修改,navigate会在状态更新后执行。


方案三:给受保护路由添加加载状态

避免在状态未确定时直接重定向,先显示加载状态,直到登录状态明确。

步骤1:在AuthContext中添加加载状态

export function AuthProvider({ children }) {
  const [session, setSession] = useState(null);
  const [isLoading, setIsLoading] = useState(true);
  const isSignedIn = !!session;

  const signIn = async ({ email, password }) => {
    setIsLoading(true);
    const { data, error } = await client.auth.signInWithPassword({ email, password });
    if (data) setSession(data.user);
    setIsLoading(false);
    return { data, error };
  };

  // 初始化时检查登录状态(可选)
  useEffect(() => {
    const checkAuth = async () => {
      const session = await client.auth.getSession();
      setSession(session?.user || null);
      setIsLoading(false);
    };
    checkAuth();
  }, []);

  return (
    <AuthContext.Provider value={{ isSignedIn, isLoading, signIn }}>
      {children}
    </AuthContext.Provider>
  );
}

步骤2:修改受保护路由组件

const ProtectedRoute = ({ children }) => {
  const { isSignedIn, isLoading } = useAuth();
  
  if (isLoading) {
    return <div>加载中...</div>; // 可替换为骨架屏或自定义加载组件
  }

  return isSignedIn ? children : <Navigate to="/sign-in" replace />;
};

为什么导航栏按钮正常工作?

导航栏不在React Router的<Outlet>下,它会在AuthContext状态更新后自动重新渲染,因此能实时读取到最新的isSignedIn值;而受保护路由是在跳转瞬间同步读取旧状态,所以触发了错误的重定向。

内容的提问来源于stack exchange,提问作者SuperJumbo

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.08.02 23:03:06