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.
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 a user
is authenticated on component render, just check for updates of isAuthenticated
from the useAuth
hook.