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} />;
}
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 onceisAuth
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
component is mounted only once while on any of its nested routes.ProtectedRoutes
starts out assuming the current user is not authenticated, so it immediately redirects to the "/login"
route.ProtectedRoutes
doesn't re-check the user's authentication when navigating between each nested route.isAuth
state to something that is neither true
for an authenticated user, and not false
for an unauthenticated user. Use undefined
to indicate the user's authentication status has not been verified yet.useLocation
hook to access the current pathname
value to be used as a useEffect
hook dependency to trigger the side-effect to check the user's authentication status.ProtectedRoutes
to conditionally render some loading UI when either the isAuth
state is not set yet or there is a pending auth check.ProtectedRoutes
to render an Outlet
, and render Layout
separately as a nested route. In other words, one route component handles access control and the other handles UI layout.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 /> },
]);