javascriptreactjsreact-hooksreact-routerreact-router-dom

I cannot figure out why ProtectedRoute wont re-render after updating useState


I would like the ProtectedRoute to call server and get a response that indicates whether the user is logged in or not, on every render of a page that it protects. The server is sent a session cookie which it uses to make the decision.

I have verified that the server is indeed returning true in my use case, which I then update state, however the ProtectedRoute does not fire again once isAuth is re-assigned from false to true. It only runs once on first render but never updates again after I get the response from the server.

This seems like it should just work out of the box, what am I missing?

ProtectedRoutes

import { lazy, useEffect, useState } from 'react';
import { Navigate } from 'react-router';
import api from '@/api';

const Layout = lazy(() => import('./Layout'));

const ProtectedRoutes = () => {
  const [isAuth, setIsAuth] = useState(false);

  useEffect(() => {
    const fetchData = async () => {
      console.log('ProtectedRoutes useEffect fired');
      try {
        const rsp = await api.request({
          url: '/is-authorized',
          method: 'GET',
        });

        if (!rsp) {
          throw new Error('No response from server');
        }

        console.log(
          'ProtectedRoutes response:',
          rsp.data?.isAuthenticated
        );

        setIsAuth(rsp.data?.isAuthenticated ?? false);
      } catch (error) {
        console.error('Error fetching authorization status:', error);
        setIsAuth(false);
      }
    };

    fetchData();
  }, []);

  console.log('isAuth:', isAuth);

  if (isAuth === true) {
    return <Layout />;
  }

  return (
    <Navigate
      to="/login"
      replace
    />
  );
};

export default ProtectedRoutes;

The browser router configuration

import { lazy } from 'react';
import { createBrowserRouter, RouterProvider } from 'react-router';
import Error from './components/Error';

const ProtectedRoutes = lazy(() => import('./components/ProtectedRoute'));
const AuthenticatePage = lazy(() => import('./components/Authenticate'));
const LoginPage = lazy(() => import('./pages/Login.page'));
const HomePage = lazy(() => import('./pages/Home.page'));
const PeoplePage = lazy(() => import('./pages/People.page'));
const PersonPage = lazy(() => import('./pages/Person.page'));
const NotFoundPage = lazy(() => import('./pages/NotFound.page'));

const router = createBrowserRouter([
  {
    path: '/',
    element: <ProtectedRoutes />,
    errorElement: <Error msg="Application Error" />,
    children: [
      {
        path: '/',
        element: <HomePage />,
      },
      {
        path: '/people',
        element: <PeoplePage />,
      },
      {
        path: '/people/:id',
        element: <PersonPage />,
      },
    ],
  },
  {
    path: '/login',
    element: <LoginPage />,
    errorElement: <Error msg="Application Error" />,
  },
  {
    path: '/authorization-code/callback',
    element: <AuthenticatePage />,
  },
  {
    path: '*',
    element: <NotFoundPage />,
  },
]);

export function Router() {
  return <RouterProvider router={router} />;
}

Solution

  • I have verified that the server is indeed returning true in my use case, which I then update state, however the ProtectedRoute does not fire again once isAuth is re-assigned from false to true. It only runs once on first render but never updates again after I get the response from the server.

    This seems like it should just work out of the box, what am I missing?

    Issues

    Solution Suggestions

    Example:

    import { useEffect, useState } from "react";
    import {
      Navigate,
      Outlet,
      useLocation,
    } from "react-router-dom";
    
    const ProtectedRoutes = () => {
      const { pathname } = useLocation();
    
      const [isAuth, setIsAuth] = useState();
      const [isLoading, setIsLoading] = useState(false);
    
      useEffect(() => {
        const fetchData = async () => {
          try {
            setIsLoading(true);
            const rsp = await api.request({
              url: "/is-authorized",
              method: "GET",
            });
    
            if (!rsp) {
              throw new Error("No response from server");
            }
    
            setIsAuth(!!rsp.data?.isAuthenticated);
          } catch (error) {
            setIsAuth(false);
          } finally {
            setIsLoading(false);
          }
        };
    
        fetchData();
      }, [pathname]);
    
      if (isLoading || isAuth === undefined) {
        return <h1>...Loading...</h1>;
      }
    
      return isAuth ? <Outlet /> : <Navigate to="/login" replace />;
    };
    
    const ProtectedRoutes = lazy(() => import('./components/ProtectedRoute'));
    const Layout = lazy(() => import('./components/Layout'));
    const AuthenticatePage = lazy(() => import('./components/Authenticate'));
    const LoginPage = lazy(() => import('./pages/Login.page'));
    const HomePage = lazy(() => import('./pages/Home.page'));
    const PeoplePage = lazy(() => import('./pages/People.page'));
    const PersonPage = lazy(() => import('./pages/Person.page'));
    const NotFoundPage = lazy(() => import('./pages/NotFound.page'));
    
    const router = createBrowserRouter([
      {
        element: <ProtectedRoutes />,
        errorElement: <Error msg="Application Error" />,
        children: [
          {
            element: <Layout />,
            children: [
              { path: '/', element: <HomePage /> },
              { path: "/people", element: <PeoplePage /> },
              { path: "/people/:id", element: <PersonPage /> },
            ],
          },
        ],
      },
      {
        path: '/login',
        element: <LoginPage />,
        errorElement: <Error msg="Application Error" />,
      },
      {
        path: '/authorization-code/callback',
        element: <AuthenticatePage />,
      },
      { path: '*', element: <NotFoundPage /> },
    ]);