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

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;

核心错误点

  1. 钩子使用违规:useState只能在函数组件顶层调用,不能放在普通异步函数内。
  2. 异步处理错误:updateAuth返回的是Promise对象,直接用loggedIn ? ...判断时,Promise会被视为“真值”,导致始终渲染受保护路由。
  3. 状态未同步:登出后组件未重新校验登录状态,也没有全局状态管理,无法感知会话销毁。
  4. 缺少加载状态:初始渲染时异步请求未完成,直接判断会导致错误跳转。

修正后的实现方案

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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.08.13 16:21:52