I wish to conditionally include authenticated routes on the routing index of my ReactJS application based on whether a token exists in the user's local storage. However, for this to work, the routes must be reevaluated on login before redirecting.
Here is the ideal usage scenario:
When the user logs in (handled by the handleLogin
function), a token is set in the browser's local storage, then the user is redirected to "/dashboard"
.
The ternary operator in router
conditionally includes private pages in the accepted routes, intended for letting authenticated users into their dashboard, for instance.
The problem:
It appears that the routes are not updated between the time the token is set and the time the user is redirected. This means that, after a user logs in, they are met with an error because the dashboard is not a valid path yet (even though it should be).
Relevant code snippets:
In LoginPage.jsx
:
const handleLogin = () => {
console.log(
`Sending login request to ${process.env.REACT_APP_API_URL}/api/auth`,
);
fetch(`${process.env.REACT_APP_API_URL}/api/auth`, {
method: "POST",
headers: {
"Content-Type": "application/json",
},
body: JSON.stringify({ email, password }),
})
.then((response) => response.json())
.then((data) => {
if (data.message === "success") {
// the login was successful
setToken(data.token);
console.log("successful login");
navigate("/dashboard");
// window.alert("The login was successful");
// get value of token using useAuth
} else {
window.alert(
`The login failed with the following error:\n\n${data.error}`,
);
}
})
.catch((error) => {
console.error("Could not login: ", error);
window.alert("An error occurred while trying to login.");
});
};
In index.jsx
:
const Routes = () => {
const { token } = useAuth();
const [ isAuthenticated, setIsAuthenticated ] = useState(!!token);
useEffect(() => {
setIsAuthenticated(!!token);
}, [token])
// route configurations go here
const routesAuthenticated = [
{
path: "/",
element: <ProtectedRoute />,
children: [
{
path: "/",
element: <UserDashboardPage />
},
{
path: "/dashboard",
element: <UserDashboardPage />
},
{
path: "/logout",
element: <LogoutPage />
},
]
}
];
const routesUnauthenticated = [
{
path: "/",
element: <LandingPage />
},
{
path: "/login",
element: <LoginPage />
},
{
path: "/about",
element: <AboutPage />
},
{
path: "/ipsum",
element: <IpsumPage />
}
];
// decide which routes are available to user based on authentication status
const router = createBrowserRouter([
// we use the ... operator to combine these arrays into one
...routesUnauthenticated,
...(isAuthenticated ? routesAuthenticated : [])
]);
// provide configuration using RouterProvider
return <RouterProvider router={router} />;
};
Attempted fix:
I tried to use the following code to force the routes to reload on login
useEffect(() => {
setIsAuthenticated(!!token);
}, [token])
Unfortunately, this does not guarantee that the routes are reloaded before handleLogin
redirects the user to their dashboard.
The basic issue is the fact that you are conditionally rendering routes, and as you have discovered, the target routes are not mounted and matchable at the time you are attempting to navigate to them. The solution is to **unconditionally render all routes and protect them accordingly.
Suggestion:
ProtectedRoute
to ensure only authenticated users are allowed access, otherwise redirect to safe unprotected route.AnonymousRoute
component to ensure only unauthenticated users are allowed to access, otherwise redirect to any safe non-anonymous route.const ProtectedRoute = () => {
const { token } = useAuth();
const location = useLocation();
return !!token
? <Outlet />
: <Navigate to="/login" replace state={{ from: location }} />;
};
const AnonymousRoute = () => {
const { token } = useAuth();
return !!token
? <Navigate to="/" replace />
: <Outlet />;
};
const Wrapper = ({
authenticatedElement = null,
unauthenticatedElement = null,
}) => {
const { token } = useAuth();
return !!token ? authenticatedElement : unauthenticatedElement
}
const router = createBrowserRouter([
{
path: "/",
element: (
<Wrapper
authentictedElement={<UserDashboardPage />}
unauthentictedElement={<LandingPage />}
/>
),
},
{
element: <ProtectedRoute />,
children: [
{
path: "/dashboard",
element: <UserDashboardPage />
},
{
path: "/logout",
element: <LogoutPage />
},
// ... other protected authenticated routes ...
]
},
{
element: <AnonymousRoute />,
children: [
{
path: "/login",
element: <LoginPage />
},
// ... other protected unauthenticated routes ...
]
},
{
path: "/about",
element: <AboutPage />
},
{
path: "/ipsum",
element: <IpsumPage />
},
// ... other unprotected routes ...
]);
const Routes = () => {
return <RouterProvider router={router} />;
};