skip to Main Content

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


  1. Chosen as BEST ANSWER

    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.


  2. What causes the re-render is validateSession and the hook below.

    What happens is, either when the validateSession is called or the useEffect, they both call a navigation to /login, which in turn re-triggers the useEffect that also calls the validateSession again, which will also fail again, hence another navigation to /login.

    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]);
    

    Extra tip: You’re trying to do too many things inside this useEffect, you can try to separate some logic here I believe.

    Login or Signup to reply.
Please signup or login to give your own answer.
Back To Top
Search