Search code examples
reactjstypescriptreact-routerreact-router-dombrowser-history

React router dom v6 - RequireAuth, ProtectedRoute , navigate(-1) need to press 2 time when refresh page


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?


Solution

  • 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}</>;
    };