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

