I am building an app in the MERN stack, and I came to a problem with my auth process, where my DOM re-renders constantly when the server returns false for isSessionValid
:
server validation:
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 });
}
};
This is my React useAuth hook which wraps my whole app:
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();
};
Per request here’s my login page:
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;
and here’s my hierarchy in App.js and 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();
Any idea why this would be happening?
lmk if you want me to provide more code to make this clearer.
2
Answers
While I like the other answer, I found the reason for the issue, the issue was that my
NavBar
component was re-rendering everything because when a user is not authenticated the navbar will check if a user is authenticated as well, if not, it will check if loading is true, if not, then it will re-route to/login
route, which was causing an infinite loop.Solution? No need for the
Navbar
to check if auser
is authenticated on component render, just check for updates ofisAuthenticated
from theuseAuth
hook.What causes the re-render is
validateSession
and thehook
below.What happens is, either when the
validateSession
is called or theuseEffect
, they both call a navigation to/login
, which in turn re-triggers theuseEffect
that also calls thevalidateSession
again, which will also fail again, hence another navigation to/login
.Extra tip: You’re trying to do too many things inside this
useEffect
, you can try to separate some logic here I believe.