Search code examples
reactjsreact-routerreact-router-dom

How can I use previous location to redirect user in react router v6?


React: 17.0.2
React Router: 6

Example: User are authenticated and try to access '/page2' through URL. They should fall through PrivateRoute flow and get to '/page2'.

Code:

const PublicRoutes = () => {
  const { auth } = useAuth()

  return auth ? <Navigate to={'/home'} replace /> : <Outlet />
}

const PrivateRoutes = () => {
  const { auth } = useAuth()

  return auth ? <Outlet /> : <Navigate to={'/signin'} replace />
}

export const Router = () => {
  return (
    <BrowserRouter>
      <Routes>
        <Route element={
          <PublicLayout>
            <PublicRoutes />
          </PublicLayout>
        }>
          <Route path='/' element={<GetStarted />} />
          <Route path='/signin' element={<SignIn />} />
          <Route path='/signup' element={<SignUp />} />
        </Route>
        <Route element={
          <PrivateLayout>
            <PrivateRoutes />
          </PrivateLayout>
        }>
          <Route path='/home' element={<Home />} />
          <Route path='/page2' element={<Page2 />} />
        </Route>
      </Routes>
    </BrowserRouter>
  )
}

I've tried const location = useLocation() and use location.pathname but it gave some errors about multiple re-renders. Even the auth guide from Router 6 didnt work because location.state is null.

Some info about my authenticante flow:

  • Using backend as service Firebase and only Google Auth at the moment.
  • When user try to signin, it will redirect to Google Auth (doc) then back to signin page, my auth context will trigger and get their data and update app auth state then will redirect to '/home' if has auth

Solution

  • The PrivateRoutes component needs to capture the current location and send this in route state to the page you are redirecting to for authentication, "/signin" in this case.

    const PrivateRoutes = () => {
      const { auth } = useAuth();
      const location = useLocation(); // <-- get current location being accessed
    
      return auth
        ? <Outlet />
        : (
          <Navigate
            to={'/signin'}
            state={{ from: location }} // <-- pass in route state
            replace
          />
        );
    };
    

    Now, somewhere on your SignIn component doing the authentication, access the passed from value from route state and redirect accordingly.

    const navigate = useNavigate();
    const location = useLocation();
    
    // Get redirect location or provide fallback
    const from = location.state?.from || "/";
    
    ...
    
    // in auth callback logic, once authenticated navigate (redirect) back 
    // to the route originally being accessed.
    navigate(from, { replace: true });
    

    I notice also your auth wrappers don't seem to account for the edge case where the app doesn't yet know the authentication status of the user, and in this case you want to conditionally render null or perhaps some loading indicator until the authContext has resolved the auth state.

    Examples:

    const PublicRoutes = () => {
      const { auth } = useAuth();
    
      if (auth === undefined) {
        return null; // or loading spinner, etc...
      }
    
      return auth ? <Navigate to={'/home'} replace /> : <Outlet />
    }
    

    ...

    const PrivateRoutes = () => {
      const { auth } = useAuth();
      const location = useLocation();
    
      if (auth === undefined) {
        return null; // or loading spinner, etc...
      }
    
      return auth
        ? <Outlet />
        : <Navigate to={'/signin'} state={{ from: location }} />;
    };