MERN栈中token无效时Auth Hook持续重渲染问题求助
MERN栈认证流程异常:Session无效时DOM持续重渲染问题分析
问题描述
使用MERN栈开发应用时,遇到认证流程异常:当服务器返回isSessionValid为false时,DOM会持续重复渲染。
相关代码
服务器端Session验证代码
const validateSession = async (req, res) => { const token = req.cookies.sessionToken; if (!token) { console.log("No token found in cookies"); return res.send({ isValidSession: false, message: "No token found in cookies" }); } try { const user = await User.findOne({ token }); if (user && user.expiresAt > new Date().getTime()) { return res.send({ isValidSession: true, userId: user.userId }); } else { console.log("Token expired or user not found"); return res.send({ isValidSession: false }); } } catch (error) { console.error("Error validating session:", error); return res.status(500).send({ isValidSession: false }); } };
React useAuth Hook代码
import React, { createContext, useContext, useState, useEffect, useCallback } from "react"; import axios from "axios"; import { useNavigate } from "react-router-dom"; export const AuthContext = createContext(); export const useAuth = () => useContext(AuthContext); export const AuthProvider = ({ children }) => { const [isAuthenticated, setIsAuthenticated] = useState(false); const [userId, setUserId] = useState(null); const [loading, setLoading] = useState(true); const navigate = useNavigate(); const validateSession = useCallback(async () => { console.log("Validating session..."); try { const response = await axios.get("http://localhost:8000/api/session/validate", { withCredentials: true }); if (response.data.isValidSession) { console.log("Session is valid"); setIsAuthenticated(true); setUserId(response.data.userId); localStorage.setItem("tokenExpiry", Date.now() + response.data.expiresIn * 1000); } else { console.log("Session is not valid"); setIsAuthenticated(false); setUserId(null); navigate("/login"); } } catch (error) { console.error("Failed to validate session:", error); setIsAuthenticated(false); setUserId(null); navigate("/login"); } finally { setLoading(false); } }, [navigate]); const handleLogout = useCallback(async () => { console.log("Logging out..."); try { await axios.post("http://localhost:8000/api/session/logout", {}, { withCredentials: true }); setIsAuthenticated(false); setUserId(null); localStorage.removeItem("userId"); localStorage.removeItem("tokenExpiry"); navigate("/login"); console.log("Logout successful"); } catch (error) { console.error("Failed to logout:", error); } }, [navigate]); const handleLogin = (navigate, userId, expiresIn) => { setIsAuthenticated(true); setUserId(userId); localStorage.setItem("tokenExpiry", Date.now() + expiresIn * 1000); navigate("/"); }; useEffect(() => { const tokenExpiry = localStorage.getItem("tokenExpiry"); const tokenExpiryNumber = Number(tokenExpiry); console.log("Token expiry:", tokenExpiry); if (!tokenExpiry || isNaN(tokenExpiryNumber) || checkTokenExpiry(tokenExpiryNumber)) { console.log("Token is either not present or expired:", tokenExpiry); handleLogout(); } else { console.log("Token is valid:", tokenExpiry); validateSession(); } const handleBeforeUnload = () => { navigator.sendBeacon("/api/session/logout"); }; let idleTimeout; const resetIdleTimer = () => { clearTimeout(idleTimeout); idleTimeout = setTimeout(() => { handleLogout(); }, 15 * 60 * 1000); // 15 minutes }; window.addEventListener("beforeunload", handleBeforeUnload); window.addEventListener("mousemove", resetIdleTimer); window.addEventListener("keypress", resetIdleTimer); resetIdleTimer(); return () => { clearTimeout(idleTimeout); window.removeEventListener("beforeunload", handleBeforeUnload); window.removeEventListener("mousemove", resetIdleTimer); window.removeEventListener("keypress", resetIdleTimer); }; }, [handleLogout, validateSession]); return ( <AuthContext.Provider value={{ isAuthenticated, userId, loading, handleLogout, handleLogin }}>{!loading && children}</AuthContext.Provider> ); }; const checkTokenExpiry = (expiry) => { return expiry < Date.now(); };
登录页面代码
import React from "react"; import axios from "axios"; import { useGoogleLogin } from "@react-oauth/google"; import { useNavigate } from "react-router-dom"; import { useAuth } from "../../../../common/hooks/useAuth"; import { LogIn } from "react-feather"; const Login = () => { const navigate = useNavigate(); const { handleLogin } = useAuth(); const login = useGoogleLogin({ clientId: process.env.REACT_APP_GOOGLE_CLIENT_ID, auto_select: true, onSuccess: async (tokenResponse) => { try { const googleUserResponse = await axios.get("https://www.googleapis.com/oauth2/v3/userinfo", { headers: { Authorization: `Bearer ${tokenResponse.access_token}`, }, }); const loginResponse = await axios.post( "http://localhost:8000/api/users/login", { token: tokenResponse.access_token, expiresAt: new Date().getTime() + tokenResponse.expires_in * 1000, email: googleUserResponse.data.email, }, { withCredentials: true } ); const userId = loginResponse.data.userId; console.log("User ID:", userId); localStorage.setItem("userId", userId); handleLogin(navigate, userId, tokenResponse.expires_in); } catch (error) { console.error("Failed to fetch user data or send to backend:", error); } }, onError: (error) => { console.error("Login Failed:", error); }, }); return ( <div className="container"> <div className="row justify-content-center align"> <div className="col-8"> <div className="card my-5"> <div className="card-body shadow"> <div className="d-flex justify-content-center mb-1"></div> <h2 className="card-title text-center mb-2">Login Page</h2> <div className="d-flex justify-content-center"> <button onClick={() => login()} className="btn btn-primary d-flex align-items-center"> Google <LogIn size={20} className="ms-1" /> </button> </div> </div> </div> </div> </div> </div> ); }; export default Login;
App.js与index.js层级代码
// App.js import "./App.css"; import Login from "./modules/components/logic/Login"; import JobDetailsScreen from "./pages/job_details/JobDetailsScreen"; import JobApplication from "./pages/job_application/JobApplication"; import Careers from "./pages/careers_page/Careers"; import { Route, Routes } from "react-router-dom"; import ProtectedRoute from "./modules/components/utils/ProtectedRoute"; import Navbar from "./modules/components/card/Navbar"; import Candidate from "./pages/candidate/Candidate"; function App() { return ( <> <Navbar /> <Routes> <Route path="/login" element={<Login />} /> <Route path="/" element={ <ProtectedRoute> <Careers /> </ProtectedRoute> } /> <Route path="/careers/:jobId" element={ <ProtectedRoute> <JobDetailsScreen /> </ProtectedRoute> } /> <Route path="/careers/apply/:jobId" element={ <ProtectedRoute> <JobApplication /> </ProtectedRoute> } /> <Route path="/user-applications/:userId" element={ <ProtectedRoute> <Candidate /> </ProtectedRoute> } /> </Routes> </> ); } export default App; //index.js import React from "react"; import ReactDOM from "react-dom/client"; import { BrowserRouter } from "react-router-dom"; import { GoogleOAuthProvider } from "@react-oauth/google"; import { AuthProvider } from "./common/hooks/useAuth"; import "./index.css"; import App from "./ui/App"; import reportWebVitals from "./reportWebVitals"; import "bootstrap/dist/css/bootstrap.min.css"; const root = ReactDOM.createRoot(document.getElementById("root")); root.render( <> <GoogleOAuthProvider clientId={process.env.REACT_APP_CLIENT_ID}> <BrowserRouter> <AuthProvider> <App /> </AuthProvider> </BrowserRouter> </GoogleOAuthProvider> </> ); reportWebVitals();
问题成因
- useEffect循环触发:
AuthProvider的useEffect依赖了handleLogout和validateSession,当token无效时调用handleLogout,该函数会更新isAuthenticated、userId状态,同时handleLogout是useEffect的依赖项,状态更新会触发useEffect重新执行,形成无限循环。 - 重复导航触发重渲染:
handleLogout和validateSession中都会调用navigate("/login"),路由切换会导致组件重新挂载,而AuthProvider的useEffect会再次执行,再次触发handleLogout,加剧循环。 - localStorage清理后的循环验证:
handleLogout会移除tokenExpiry,useEffect每次执行都会检查tokenExpiry,此时tokenExpiry不存在,又会调用handleLogout,形成闭环。
内容的提问来源于stack exchange,提问作者Liemannen loop
相关产品推荐
相关产品推荐

