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
相关产品推荐
相关产品推荐

