javascriptnode.jsreactjsauthenticationweb

Why does react setState does not work as expected?


im creating a wrapper to check authentication the server response is as expected but somehow the isAuthenticated state does not change to true.

import React, { useState, useEffect } from "react";
import { Navigate } from "react-router-dom";
import Cookies from "js-cookie";
import axios from "axios";

function PrivateRoute({ children }) {
  const [isAuthenticated, setIsAuthenticated] = useState(false);

  async function checkAuth() {
    try {
      axios.defaults.withCredentials = true;
      const sessionId = Cookies.get("sessionId");
      const data = {
        sessionId: sessionId,
      };
      if (sessionId) {
        const response = await axios.get("http://127.0.0.1:5000/session", {
          params: data,
        });
        console.log(response.status);
        if (response.status === 200) {
          console.log("authenticated");
          setIsAuthenticated(true);
        } else {
          console.log("unauthenticated");
          setIsAuthenticated(false);
        }
      }
    } catch (err) {
      console.error(err);
      setIsAuthenticated(false);
    }
  }

  checkAuth();
  console.log(isAuthenticated);
  return isAuthenticated ? children : <Navigate to="/login" />;
}

export default PrivateRoute;


ive tried to log inside my if statement and it works and when i log isAuthenticated after calling the checkAuth function it remains false.


Solution

  • As others have mentioned, React batches state updates in order to optimize performance, meaning setState runs "asynchronously" (though it's not a Promise-returning function).

    If you're not using a client-side caching library like useSWR, then per the React documentation, the proper approach is to do data fetching within a useEffect(). Just make sure to have an ignore flag to avoid potential race conditions, as well as add support for a null state for when the request is still loading (a.k.a. before you decide to give the user access or navigate them away). In the end, your component should look something like this:

    import React, { useState, useEffect } from "react";
    import { Navigate } from "react-router-dom";
    import Cookies from "js-cookie";
    import axios from "axios";
    
    function PrivateRoute({ children }) {
        const [isAuthenticated, setIsAuthenticated] = useState(null);
        const sessionId = Cookies.get("sessionId");
    
        useEffect(() => {
            let ignore = false;
            async function checkAuth() {
                try {
                    axios.defaults.withCredentials = true;
                    if (sessionId) {
                        const response = await axios.get("http://127.0.0.1:5000/session", { params: { sessionId } });
                        const isSuccess = response.status === 200;
                        if (!ignore) {
                            console.log(isSuccess ? "authenticated" : "unauthenticated");
                            setIsAuthenticated(isSuccess);
                        }
                    }
                } catch (err) {
                    if (!ignore) {
                        console.error(err);
                        setIsAuthenticated(false);
                    }
                }
            }
    
            checkAuth();
            return () => { ignore = true; };
        }, [sessionId]);
    
        console.log(isAuthenticated);
    
        if (isAuthenticated === null) return "Loading...";
    
        return isAuthenticated ? children : <Navigate to="/login" />;
    }
    
    export default PrivateRoute;