In React Router, how do you write a RequireAuth wrapper that sends signed-out users to /login while remembering the page they asked for?
answer
- a component that renders instead of children
- Navigate, not navigate() in render
- replace the history entry
- location goes into state
basics
~10 sA RequireAuth component reads the auth state and useLocation(); when signed out it returns <Navigate to="/login" replace state={{ from: location }} />, otherwise its children or <Outlet />. The login page later reads location.state.from.
solid answer
~40 sThe guard is an ordinary component. It reads the current user from your auth context and the current location with `useLocation()`. If there is no user it returns `<Navigate to="/login" replace state={{ from: location }} />`; otherwise it returns `children`, or `<Outlet />` when it is used as a layout route wrapping the whole `/admin` subtree. `replace` swaps the protected entry for `/login` so Back does not bounce the user into the guard again. `state` travels with the new history entry, so the login page can read `location.state?.from` and return there. Guards return `<Navigate>` because calling `navigate()` during render is not allowed — React Router warns to call it in an effect, and `<Navigate>` does exactly that. Remember it is UX only: the API still has to authorize every request.
code
tsx · 24 linesimport { Navigate, Outlet, Route, Routes, useLocation } from "react-router";
import { useAuth } from "./auth";
import { AdminHome, AdminUsers, LoginPage } from "./pages";
function RequireAuth() {
const { user } = useAuth();
const location = useLocation();
if (!user) {
return <Navigate to="/login" replace state={{ from: location }} />;
}
return <Outlet />;
}
export function AppRoutes() {
return (
<Routes>
<Route path="/login" element={<LoginPage />} />
<Route element={<RequireAuth />}>
<Route path="/admin" element={<AdminHome />} />
<Route path="/admin/users" element={<AdminUsers />} />
</Route>
</Routes>
);
}go deeper
Be able to write the component: useLocation, a Navigate with replace and state when signed out, Outlet or children otherwise.
Explain why Navigate is used instead of navigate() in render, what replace does to history, and where state is stored.
Place the guard as a layout route with public pages outside it, handle the unknown-session case, and recognise that in a data router loaders run before a wrapper can act.
Decide where access checks live across the app: wrapper guards for UX, loader or middleware guards for data routers, and mandatory authorization on every API.
## The pattern A **route guard** decides whether a navigation may show a screen. In React Router's declarative style the guard is a plain React component — commonly called `RequireAuth` — that renders either the protected content or a redirect. It uses three React Router pieces: - **`useLocation()`** — returns the current `location` object (`pathname`, `search`, `hash`, `state`, `key`). - **`<Navigate>`** — a component that performs a navigation when it renders. Its props are `to`, `replace`, `state` and `relative`. - **`<Outlet />`** — renders the matched child route, so one guard placed on a parent can protect a whole subtree. ## Why each piece is there | Piece | What it does | What goes wrong without it | |---|---|---| | `useLocation()` | captures where the user was going | the login page cannot send them back | | `<Navigate to="/login">` | redirects from inside render safely | calling `navigate()` in the render body triggers React Router's "call navigate() in a React.useEffect()" warning | | `replace` | replaces the protected entry in the history stack | Back returns to `/admin/users`, the guard fires again, and the user is stuck in a loop | | `state={{ from: location }}` | stores the intended destination on the new history entry | the destination is lost, or has to be put in the URL | | `children` / `<Outlet />` | renders the protected content when allowed | the guard can only wrap one element instead of a subtree | `<Navigate>` performs its navigation from an effect after it renders, so the guard's render output for a signed-out user is just the `<Navigate>` element: the protected children are never rendered. ## Protecting the `/admin` subtree Use the guard as a **layout route** — a route with an element but no path — so every admin page is covered by one component: 1. Define a route whose element is `<RequireAuth />` and give it the admin routes as children. 2. Inside `RequireAuth`, return `<Outlet />` when the user is signed in. 3. Keep `/login` **outside** that layout route; if the login page were inside it, the guard would redirect the login page to itself. The same shape works with route objects in a data router: `{ element: <RequireAuth />, children: [...] }`. ## Reading the destination on the login page `location.state` is whatever was passed as `state` — here the whole previous `location`. The login page reads `location.state?.from?.pathname` (plus `search` if you need it), and after a successful sign-in calls `navigate(from, { replace: true })`, falling back to a default such as `/` when `state` is empty. Returning the user, and validating a destination that comes from the URL instead, are covered in their own question. ## Limits of a wrapper guard 1. **It is a UX feature, not security.** Anyone can call the API directly or edit client code; every admin endpoint must check the session and the role itself. 2. **`state` lives only in that history entry.** It is not part of the URL, so it does not survive opening the login link in a new tab or sharing it. 3. **In a data router, a wrapper runs after loaders.** The router runs a route's loaders before rendering any of its components, so admin loaders have already fetched by the time `RequireAuth` renders. Data-router apps usually guard in a loader or middleware instead. 4. **Unknown auth state needs its own branch.** If the session is still loading, render a placeholder rather than redirecting, or a signed-in user is bounced to `/login` on reload. ## Role checks in the same wrapper The same component shape handles authorization for the UI: read the user's roles, and for a signed-in user without the `admin` role render a "not allowed" screen or `<Navigate to="/" replace />` instead of `<Outlet />`. Keep the two branches distinct — signed out goes to `/login` with `state`, signed in but not permitted does **not**, or the login page would bounce a signed-in user in circles. ## Common mistakes - Omitting `replace`, which creates the Back-button loop. - Putting `/login` inside the protected layout, which creates a redirect loop. - Storing only `location.pathname` and losing the query string the user needed. - Treating the guard as the only access control. In an interview, write the eight-line component, then name `replace`, `state` and "UX, not security" without being asked.
- Why not call navigate('/login') directly in the guard's body?Navigating during render is a side effect in the middle of React's render phase. React Router warns that `navigate()` should be called in a `React.useEffect()`, not on first render. `<Navigate>` wraps exactly that: it renders nothing and navigates from an effect, which is why guards return it.
- What does the user experience if you forget replace on the Navigate?History holds the protected URL followed by `/login`. Pressing Back from the login page returns to the protected URL, the guard redirects to `/login` again, and the user cannot get back to where they came from. `replace` swaps the protected entry out, so Back leaves the flow cleanly.
- Why must /login sit outside the RequireAuth layout route?The guard redirects every signed-out visit to its children. If `/login` were a child, a signed-out user visiting `/login` would be redirected to `/login` again, forever. Public routes such as login, sign-up and password reset belong outside the protected layout.
saying these in an interview costs you the question
- Calling navigate('/login') directly in the component body is the normal way
- replace is only cosmetic and does not affect the Back button
- location.state survives when the login link is opened in a new tab
- Hiding the admin route in the client is enough to protect admin data
- Each protected page needs its own copy of the guard component