I'm building a project on react. The project has authentication. I use json web token (stateless auth). Also I'm using react-router-dom v6 for handling navigation.
The problem that I want to solve is checking auth when navigating through private routes.
Let's say the jwt expires when the user is on a private route, then the user navigates to another private route, I want the user to be redirected to the login page, therefore the way that I'm facing this problem is checking the validity of the jwt everytime a new private route is rendered. Apparently react-router-dom is using the same previous private route and the state for isAuthenticated variable still true.
I've also thought about use auth context but I'm using refresh token logic implemented via axios, so I cannot access the auth context outside a react component to remove the user from context when the refresh token expire.
How can I handle this problem? Is the best solution the one that I'm telling you? Why the private route is not rerendering again when navigating to call useEffect again and check the token?
I hope u can help me.
This is the PrivateRoute logic
export default function PrivateRoute({ children }) {
const userData = useUserData();
const api = useContext(ApiContext)
const auth = useContext(AuthContext)
const [isAuthenticated, setIsAuthenticated] = useState(null);
const checkAuth = async () => {
try {
const res = await axiosBase.post('/auth/token/verify',{
token: localStorage.getItem('jwt-access')
});
if (res.status == 200) {
setIsAuthenticated(true)
} else {
setIsAuthenticated(false)
}
} catch (e) {
console.error(e)
setIsAuthenticated(false)
}
}
useEffect(() => {
checkAuth()
}, [])
if (isAuthenticated == null) {
return (<div className=''> Loading...</div>)
}
return (
!isAuthenticated ? <Navigate to={'/login'} /> : (
children || <Outlet />
)
)
}
And this is how I organize the routes
<Routes>
<Route path='/' element={<Layout></Layout>}>
<Route index element={<HomePage />} />
/** ADMIN MODULE */
<Route path='/admin' element={<PrivateRoute></PrivateRoute>}>
<Route path='newClients' element={
<NewClients />
} />
</Route>
/** CLIENTS MODULE */
<Route path='/clients'>
<Route path='services' element={
<PrivateRoute>
<Services />
</PrivateRoute>
} />
<Route path='myServices' element={
<PrivateRoute>
<MyServices />
</PrivateRoute>
} />
</Route>
<Route path='/perfil'>
<Route index element={<PrivateRoute><Profile /></PrivateRoute>} />
</Route>
</Route>
<Route>
<Route path='*' element={<HomePage />} />
</Route>
<Route path='login' element={<Login />} />
<Route path='signup' element={<SignUp />} />
</Routes>
Apparently
react-router-domis using the same previous private route and the state forisAuthenticatedvariable still true.
This is true when PrivateRoute is rendered as a layout route and rendering several nested routes. The same PrivateRoute instance only runs the useEffect hook once since it has an empty dependency array, so no additional auth checks are done.
Separately when rendering several PrivateRoute components as wrapper components or layout route components, they each have their own isAuthenticated state. Centralizing the isAuthenticated state in a context is a way to force all PrivateRoute components to reference the same single source of truth.
The main issue is that the PrivateRoute doesn't rerun the auth check when the route changes, it only does so when it mounts.
You should add a "loading" state to the PrivateRoute that is active while an auth check is occurring and use the current location as a dependency for the useEffect hook to trigger the auth check.
Example:
import { Outlet, Navigate, useLocation } from 'react-router-dom';
function PrivateRoute({ children }) {
const { pathname } = useLocation();
...
const [isAuthenticated, setIsAuthenticated] = useState();
// add loading state, initially true for initial render
const [isLoading, setIsLoading] = useState(true);
const checkAuth = async () => {
setIsLoading(true); // <-- set true when starting auth check
try {
const res = await axiosBase.post("/auth/token/verify", {
token: localStorage.getItem("jwt-access")
});
setIsAuthenticated(res.status == 200);
} catch (e) {
console.error(e);
setIsAuthenticated(false);
} finally {
setIsLoading(false); // <-- clear loading state when completed
}
};
useEffect(() => {
checkAuth();
}, [pathname]); // <-- check auth status on mount/when location changes
if (isLoading) {
return <div className="">Loading...</div>;
}
return isAuthenticated ? children || <Outlet /> : <Navigate to={"/login"} />;
}
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With