So I'm using React with react-router-dom@6.3
. Here is what my project flow look like.
I have a authRoutes
(public route) and a cmsRoutes
(protected route).
const cmsRoutes: RouteObject[] = [
{ path: '/', element: <DashboardContainer /> },
]
const CmsRender = () => useRoutes(cmsRoutes);
const authRoutes: RouteObject[] = [
{
path: '/*',
element: (
<RequireAuth>
<CmsLayout />
</RequireAuth>
),
caseSensitive: true,
}
]
This is my <App/>
file it render a container
const App = () => {
return (
<ErrorBoundary>
<Provider store={store}>
<HashRouter>
<TheContainer />
</HashRouter>
</Provider>
</ErrorBoundary>
);
};
TheContainer
then render the authRoute
(public route)
const loading = (
<div className="pt-3 text-center">
<div className="sk-spinner sk-spinner-pulse"></div>
</div>
);
const TheContent = () => {
const dispatch = useDispatch();
const { token } = useSelector((state: RootState) => state.authentication);
useEffect(() => {
let tempToken = token;
if (!tempToken) {
tempToken = localStorage.getItem('authentication_token');
}
if (tempToken) {
// get current user info
dispatch(fetching());
dispatch(account());
}
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [token]);
return (
<Suspense fallback={loading}>
<RouteRender />
</Suspense>
);
};
export default React.memo(TheContent);
The path "/" will route to <TheContent/>
Component where i render the cmsRoutes
(Protected Route)
const loading = (
<div className="pt-3 text-center">
<div className="sk-spinner sk-spinner-pulse"></div>
</div>
);
const TheContent = () => {
return (
<CContainer fluid className="px-0">
<Suspense fallback={loading}>
<CmsRender />
</Suspense>
</CContainer>
);
};
export default React.memo(TheContent);
Before render the cmsRoutes
the code will go through the <RequireAuth/>
Component.
If the user is login it will render cmsRoutes
other wise it will redirect the user to the <Login/>
component and also send the current location with it through the state={{ path: location.pathname }}
.
interface IRequireAuthProp {
children: React.ReactNode;
}
export const RequireAuth = ({ children }: IRequireAuthProp) => {
const { location } = useRouter();
const { user } = useSelector((state: RootState) => state.authentication);
if (!user) {
return <Navigate to="/login" state={{ path: location.pathname }} />;
}
return <>{children}</>;
};
Here is my <Login/>
component. So if user login success it will redirect user to the cmsRoutes
. If the state url exist it will redirect to that url and then replace the url in the history stack
interface ILocationPath {
path?: string
}
const Login = () => {
const { navigate, location } = useRouter()
const state = location.state as ILocationPath
useEffect(() => {
if (user) {
const statePath = state?.path || '/'
const redirectPath = statePath.includes('login') ? '/' : statePath
navigate(redirectPath, { replace: true })
}
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [user])
return (
<>Login logic</>
)
}
export default Login
HERE'S THE PROBLEM:
When I enter a page in the cmsRoutes
eg: I'm in the Manage
page with a table of users, then I redirect to a Detail
page of a user. In the Detail
page have a button onClick
navigate(-1)
to redirect back to the previous page, everything work fine. Until I refresh (f5) the page. Because of the above flow my web then will go through the <RequireAuth />
component first but that moment it does not find user exist it will redirect to the <Login />
. In the <Login />
the code find the user does exist then it will redirect back to the Detail
page. Every time it will add the "/login"
path to the history stack, because I use replace: true
the stack will replace the "/login"
path with the path of the Detail
page. Every time the page refresh (f5) the history stack will add more of the Detail
page then the navigate(-1)
will have to go through all the Detail
page have been stack before go back to the Manage
page. If I don't use replace: true
then the navigate(-1)
will go back to the <Login />
and the <Login />
then redirect back to the current page it will be a loop back and forth. So any idea how to resolve this or do you guys use another way to redirect with protected route?
I think all you need to fix is to actually redirect to "/login"
for unauthenticated users. Instead of a PUSH navigation action it should be a REPLACE navigation action. This will help maintain the history stack.
export const RequireAuth = ({ children }: React.PropsWithChildren<{}>) => {
const { location } = useRouter();
const { user } = useSelector((state: RootState) => state.authentication);
if (!user) {
return (
<Navigate
to="/login"
replace // <-- redirect
state={{ path: location.pathname }}
/>
);
}
return <>{children}</>;
};