Protected routes and auth in React
Wrap protected routes in a guard component that reads auth state from a context/store and either renders the children, redirects to `/login` (with a `?next` param so post-login lands on the requested page), or renders a loading state during the auth probe. For role-based access, wrap a second guard around routes that require specific roles. The frontend guard is **UX**, not security — every protected API endpoint must enforce auth server-side independently.
Two questions: (1) how do you make the right pages visible to the right users, (2) what does that not protect against.
The frontend pattern
function RequireAuth({ children }: { children: React.ReactNode }) {
const { status, user } = useAuth();
const location = useLocation();
if (status === "loading") return <FullPageSpinner />;
if (status === "unauthenticated") {
return <Navigate to={`/login?next=${encodeURIComponent(location.pathname)}`} replace />;
}
return <>{children}</>;
}
function RequireRole({ role, children }: { role: Role; children: React.ReactNode }) {
const { user } = useAuth();
if (!user?.roles.includes(role)) return <Forbidden />;
return <>{children}</>;
}
// usage
<Route path="/dashboard" element={<RequireAuth><Dashboard /></RequireAuth>} />
<Route path="/admin" element={<RequireAuth><RequireRole role="admin"><Admin /></RequireRole></RequireAuth>} />Three states, not two: loading is the one people forget. During the initial auth probe (cookie present, hit /me), you don't know yet — rendering "unauthenticated → redirect to /login" prematurely will redirect already-logged-in users.
Auth state in context
function AuthProvider({ children }) {
const [state, setState] = useState({ status: "loading" });
useEffect(() => {
api.get("/me")
.then(user => setState({ status: "authenticated", user }))
.catch(() => setState({ status: "unauthenticated" }));
}, []);
return <AuthContext value={state}>{children}</AuthContext>;
}Or, in 2026, use TanStack Query for the /me fetch (caching, retry, refetch on focus all come for free).
Post-login redirect
The ?next= param ensures clicking a deep link, getting bounced to login, and authenticating lands them back where they wanted:
function LoginPage() {
const [params] = useSearchParams();
const next = params.get("next") || "/";
const navigate = useNavigate();
async function onSubmit(creds) {
await api.login(creds);
navigate(next, { replace: true });
}
...
}Validate next. Reject external URLs to prevent open-redirect (?next=https://evil.com). Allow only path-relative URLs.
Role-based / permission-based access (RBAC)
Two flavors:
- Roles — coarse (admin, member, viewer).
- Permissions — fine (
order:write,user:delete).
Permissions scale better. UI helper:
function Can({ permission, children, fallback = null }) {
const { user } = useAuth();
return user?.permissions?.includes(permission) ? <>{children}</> : fallback;
}
// usage
<Can permission="user:delete">
<button onClick={deleteUser}>Delete</button>
</Can>Use Can to hide actions; use route guards to gate pages. Don't only hide buttons — a user can still hit the API endpoint directly.
Server-side rendering (Next.js)
// middleware.ts — runs at the edge, before the page renders
export function middleware(req: NextRequest) {
const token = req.cookies.get("session")?.value;
if (!token && req.nextUrl.pathname.startsWith("/app")) {
const url = req.nextUrl.clone();
url.pathname = "/login";
url.searchParams.set("next", req.nextUrl.pathname);
return NextResponse.redirect(url);
}
}Better than client-side guards for protected SSR pages because:
- No flash of the protected page before redirect.
- Search engines don't index the protected route's HTML.
- The protected page's data fetch can rely on the user being authenticated.
The 5 mistakes interviewers look for
1. Treating the frontend guard as security. It isn't. A user can edit JS in DevTools and force <RequireAuth> to render its children. The API must independently enforce auth on every endpoint. The frontend guard is for UX — showing the right pages.
2. Missing the loading state. Premature redirect flickers users who are actually authenticated.
3. Re-fetching /me on every navigation. Cache it; refetch only on focus/reconnect/logout.
4. Storing tokens in localStorage. XSS-reachable. Use HttpOnly cookies.
5. No logout strategy. What happens to in-flight requests, query cache, open WebSockets, other tabs? On logout: clear query cache, close WebSockets, broadcast to other tabs, redirect to login.
function logout() {
api.post("/logout").catch(() => {});
queryClient.clear();
realtime.disconnect();
bc.postMessage({ type: "logout" }); // tell other tabs
window.location.href = "/login";
}Cross-tab session sync
useEffect(() => {
const bc = new BroadcastChannel("auth");
bc.onmessage = (e) => {
if (e.data.type === "logout") window.location.reload();
if (e.data.type === "login") refetchMe();
};
return () => bc.close();
}, []);A user logging out in one tab should be logged out everywhere.
Senior framing
The interviewer is testing:
- Three auth states, not two.
?next=for deep-link UX, with open-redirect validation.- RBAC pattern with reusable guards.
- SSR/middleware guards when applicable.
- Logout cleanup across cache, WebSockets, tabs.
- Explicit acknowledgment that the frontend guard is UX, not security.
The "I wrap protected routes in <RequireAuth>" answer is mid. The full answer above is senior.
Follow-up questions
- •Why is the loading state critical?
- •Why is hiding a delete button not enough for protection?
- •How does middleware-based auth in Next.js avoid SSR flash?
- •What needs to happen on logout besides clearing the token?
Common mistakes
- •Premature redirect to login during auth probe.
- •Trusting frontend role checks for security.
- •Storing tokens in localStorage.
- •No cross-tab logout sync.
Performance considerations
- •Cache /me — don't refetch on every navigation.
- •Middleware auth at the edge avoids SSR cost for unauthenticated users.
Edge cases
- •Token expired mid-session — 401 should trigger refresh, not redirect on every request.
- •Multi-tab login — broadcast to refetch.
- •Server clock drift causing token-not-yet-valid errors.
Real-world examples
- •Next.js middleware for /app prefix protection.
- •Auth0, Clerk, Supabase Auth all provide the AuthProvider + guard pattern.