I have written the following code in react development that runs without an error. But it appears buggy in a peculiar way. A race condition when running useEffect
looks possible:
// Variables declared top-level outside the component
let initialized = false;
let componentIsMounted = true;
const PrivateRouteGuard = () => {
const token = useSelector((state) => state?.auth?.token);
const userIsAuthenticated = Boolean(token) || sessionStorage.getItem("isAuthenticated");
const [displayPage, setDisplayPage] = useState(false);
const dispatch = useDispatch();
useEffect(() => {
componentIsMounted = true;
if (!initialized) {
initialized = true;
console.log("✅ Only runs once per app load");
if (!token && userIsAuthenticated) {
refreshAccessToken()
.then((response) => {
// Add the new Access token to redux store
dispatch(addAuthToken({ token: response?.data?.accessToken }));
return getUserProfile(); // Get authenticated user using token in redux store
})
.then((response) => {
const user = response.data?.user;
// Add authenticated user to redux store
dispatch(addAuthUser({ user }));
})
.finally(() => {
componentIsMounted && setDisplayPage(true);
});
} else {
setDisplayPage(true);
}
}
return () => componentIsMounted = false;
}, []);
if (!displayPage) {
return "LOADING..."; // Display loading indicator here
}
if (!userIsAuthenticated) {
return (
<Navigate to="/login" />
);
}
return <Outlet />;
};
export default PrivateRouteGuard;
With the code above, a user with an authentication token is considered authenticated. I have backed up authentication status in browser local storage since a page refresh, wipes out redux store.
Actually this is a react component that protects a route from an unauthenticated user.
With how react strict mode works in development, useEffect is called twice. However calling twice to get a new Access token (refreshAccessToke()
), will cause a token to be invalidated(how the backend works). So I tried to mitigate useEffect from calling refreshAccessToken()
twice by using a top-level initialized
variable.
While this works, the code makes me cringe abit.
Why?
On page refresh:
useEffect
runs andrefreshAccessToken()
is fired on the first app load.- React cleans Up
useEffect
by running clean up function. - React sets up
useEffect
again; this time not callingrefreshAccessToken()
.
The problem
However I am doubtful of a race condition occuring at step 2
. Say, while React is running clean-up function, refreshAccessToken()
has resolved and is ready with its data. But when clean up function is run,the variable componentIsMounted
is false
. Therefore this piece of code:
.finally(() => {
componentIsMounted && setDisplayPage(true);
})
Means the state will never be set.
React then heads to step 3
, where everything is setup again. But refreshAccessToken()
finished running(while at step 2
) and it already decided not to set displayPage
state i.e: componentIsMounted && setDisplayPage(true);
. So there is a possibility to get stuck seeing a loading indicator as with this piece:
if (!displayPage) {
return "LOADING..."; // Display loading indicator here
}
I might be missing a concept about the internals of react. Nevertheless code shared above looks like one that sometimes work and sometimes fail. I hope to get my doubts cleared.