React Router v6:登出后仍可访问受保护路由问题求助
问题描述
在React中实现私有路由时遇到以下问题:
- 需求:用户登录后才能访问首页等受保护路由;重启前端后,受保护路由需重新登录才可访问。
- 当前异常:首次登录后执行登出(后端会话已销毁,接口返回
{isLoggedIn: false}),仍能访问受保护路由;未使用useState时,登录后后端确认已登录,但无法访问任何受保护路由。
原代码问题分析
私有路由(原实现)
import { useState, useEffect } from 'react'; import React from 'react'; import axios from 'axios'; const checkIfLogged = async () => { let[logged, setLogged] = useState(false); // 错误:钩子不能在普通函数内调用 await axios.get("http://localhost:3001/auth", { withCredentials: true }).then((res) => { setLogged(res.data.isLoggedIn); }) return logged; } const updateAuth = async(check) => { const loggedIn = await check; return loggedIn; } const PrivateRoutes = () =>{ const loggedIn = updateAuth(checkIfLogged); // 错误:返回的是Promise对象,不是布尔值 return( loggedIn ? <Outlet /> : <Navigate to="/login" /> ) } export default PrivateRoutes;
核心错误点
- 钩子使用违规:
useState只能在函数组件顶层调用,不能放在普通异步函数内。 - 异步处理错误:
updateAuth返回的是Promise对象,直接用loggedIn ? ...判断时,Promise会被视为“真值”,导致始终渲染受保护路由。 - 状态未同步:登出后组件未重新校验登录状态,也没有全局状态管理,无法感知会话销毁。
- 缺少加载状态:初始渲染时异步请求未完成,直接判断会导致错误跳转。
修正后的实现方案
1. 创建全局Auth上下文(同步登录状态)
// src/contexts/AuthContext.js import { createContext, useContext, useState, useEffect } from 'react'; import axios from 'axios'; const AuthContext = createContext(); export const AuthProvider = ({ children }) => { const [isLoggedIn, setIsLoggedIn] = useState(false); const [loading, setLoading] = useState(true); // 初始化校验登录状态 useEffect(() => { const checkAuthStatus = async () => { try { const res = await axios.get("http://localhost:3001/auth", { withCredentials: true }); setIsLoggedIn(res.data.isLoggedIn); } catch (err) { setIsLoggedIn(false); } finally { setLoading(false); } }; checkAuthStatus(); }, []); // 登出操作:调用后端接口后更新状态 const logout = async () => { try { await axios.get("http://localhost:3001/logout", { withCredentials: true }); setIsLoggedIn(false); } catch (err) { console.error("登出失败", err); } }; // 登录成功后更新状态 const loginSuccess = () => { setIsLoggedIn(true); }; return ( <AuthContext.Provider value={{ isLoggedIn, loading, loginSuccess, logout }}> {children} </AuthContext.Provider> ); }; export const useAuth = () => useContext(AuthContext);
2. 修正私有路由组件
// src/utils/private.js import { Outlet, Navigate } from 'react-router-dom'; import { useAuth } from '../contexts/AuthContext'; const PrivateRoutes = () => { const { isLoggedIn, loading } = useAuth(); // 加载中显示过渡组件,避免页面闪烁 if (loading) { return <div>加载中...</div>; } return isLoggedIn ? <Outlet /> : <Navigate to="/login" replace />; }; export default PrivateRoutes;
3. 登录组件(同步状态)
// src/screens/Login.js import { useState } from 'react'; import axios from 'axios'; import { useAuth } from '../contexts/AuthContext'; import { useNavigate } from 'react-router-dom'; const Login = () => { const [email, setEmail] = useState(''); const [password, setPassword] = useState(''); const { loginSuccess } = useAuth(); const navigate = useNavigate(); const handleLogin = async (e) => { e.preventDefault(); try { await axios.post("http://localhost:3001/login", { email, password }, { withCredentials: true }); loginSuccess(); // 更新全局登录状态 navigate('/'); } catch (err) { console.error("登录失败", err); } }; return ( <form onSubmit={handleLogin}> <input type="email" value={email} onChange={(e) => setEmail(e.target.value)} placeholder="邮箱" /> <input type="password" value={password} onChange={(e) => setPassword(e.target.value)} placeholder="密码" /> <button type="submit">登录</button> </form> ); }; export default Login;
4. 登出组件(同步状态)
// src/screens/Logout.js import { useEffect } from 'react'; import { useAuth } from '../contexts/AuthContext'; import { useNavigate } from 'react-router-dom'; const Logout = () => { const { logout } = useAuth(); const navigate = useNavigate(); useEffect(() => { const handleLogout = async () => { await logout(); navigate('/login'); }; handleLogout(); }, [logout, navigate]); return <div>正在登出...</div>; }; export default Logout;
5. 包裹App组件
// src/App.js import { Routes, Route } from "react-router-dom"; import React from "react"; import { AuthProvider } from './contexts/AuthContext'; import Home from "./screens/Home"; import About from "./screens/About"; import Login from "./screens/Login"; import Register from "./screens/Register"; import Logout from "./screens/Logout"; import PrivateRoutes from "./utils/private"; const App = () => { return ( <AuthProvider> <Routes> <Route element={<PrivateRoutes />}> <Route path="/" exact element={<Home />} /> <Route path="/about" element={<About />} /> </Route> <Route path="/login" element={<Login />} /> <Route path="/register" element={<Register />} /> <Route path="/logout" element={<Logout />} /> </Routes> </AuthProvider> ); }; export default App;
关键改进说明
- 用全局Context管理登录状态,确保登录、登出、私有路由共享同一状态,实现状态同步。
- 初始化时校验登录状态,处理加载阶段,避免页面闪烁或错误跳转。
- 登出时主动更新全局状态,私有路由能立即感知会话销毁,跳转到登录页。
内容的提问来源于stack exchange,提问作者IdotEXE
相关产品推荐
相关产品推荐

