Protected Routes
Some pages are only for signed-in users, and some only for admins. A small wrapper route checks the user and redirects everyone else — then sends them back after they log in.
On this page · 6 min · Advanced
On this page
- Learning objectives
- Explanation
- Syntax
- Code example
- Step by step
- Real-world use
- Common mistakes
- Best practices
- Remember this
- Practice exercise
- Mini quiz
- Related topics
- My notes
- Reading time
- 6 min
- Difficulty
- Advanced
- Updated
- Oct 28, 2026
Related tutorials
Learning objectives#
- Write a RequireAuth wrapper route
- Redirect to login and back again
- Restrict routes by role
- Handle the “still checking” state
Explanation#
A protected route is a component that renders <Outlet /> (or children) when the user is allowed, and <Navigate to="/login" replace /> otherwise.
Pass the current location in navigation state: <Navigate to="/login" state={{ from: location }} />. After login, navigate(from ?? "/") returns users to the page they wanted.
While the session is still being checked, render a loading indicator — not the redirect — or signed-in users get bounced to login on every refresh.
Role-based routes add a check such as user.roles.includes("Admin") and show a 403 page or redirect.
Route guards only improve the experience. The API must still reject unauthorised requests.
Syntax#
function RequireAuth() {
const { user, checking } = useAuth();
const location = useLocation();
if (checking) return <p>Loading…</p>;
if (!user) return <Navigate to="/login" state={{ from: location }} replace />;
return <Outlet />;
}
Code example: Guarding account and admin pages#
import { createContext, useContext, useState } from 'react';
import {
createBrowserRouter, RouterProvider, Outlet, Navigate, Link,
useLocation, useNavigate,
} from 'react-router';
const AuthContext = createContext(null);
const useAuth = () => useContext(AuthContext);
function AuthProvider({ children }) {
const [user, setUser] = useState(null);
const login = (name, roles) => setUser({ name, roles });
const logout = () => setUser(null);
return <AuthContext value={{ user, checking: false, login, logout }}>{children}</AuthContext>;
}
function RequireAuth({ role }) {
const { user, checking } = useAuth();
const location = useLocation();
if (checking) return <p>Checking session…</p>;
if (!user) return <Navigate to="/login" state={{ from: location }} replace />;
if (role && !user.roles.includes(role)) return <p role="alert">403 — you do not have access.</p>;
return <Outlet />;
}
function LoginPage() {
const { login } = useAuth();
const navigate = useNavigate();
const from = useLocation().state?.from?.pathname ?? '/';
return (
<>
<button onClick={() => { login('Asha', ['Customer']); navigate(from, { replace: true }); }}>Log in as customer</button>
<button onClick={() => { login('Ravi', ['Admin']); navigate(from, { replace: true }); }}>Log in as admin</button>
</>
);
}
function Layout() {
const { user, logout } = useAuth();
return (
<>
<nav>
<Link to="/">Home</Link> | <Link to="/account">Account</Link> | <Link to="/admin">Admin</Link>
{user && <> | {user.name} <button onClick={logout}>Log out</button></>}
</nav>
<Outlet />
</>
);
}
const router = createBrowserRouter([
{
element: <Layout />,
children: [
{ path: '/', element: <p>Public home page</p> },
{ path: '/login', element: <LoginPage /> },
{ element: <RequireAuth />, children: [{ path: '/account', element: <p>Your orders</p> }] },
{ element: <RequireAuth role="Admin" />, children: [{ path: '/admin', element: <p>Admin dashboard</p> }] },
],
},
]);
export default function App() {
return <AuthProvider><RouterProvider router={router} /></AuthProvider>;
}
Visiting /account while signed out redirects to /login. Clicking “Log in as customer” returns you to /account (“Your orders”). Opening /admin as the customer shows “403 — you do not have access.”; logging in as admin shows “Admin dashboard”.
This example is illustrative. Copy or download it and run it in your own environment; the expected output is shown above.
Step-by-step explanation#
- RequireAuth is a layout route: it either renders its children via <Outlet /> or redirects.
- The redirect stores the attempted location in navigation state.
- LoginPage reads state.from and navigates back after logging in; replace avoids a back-button loop.
- The admin group passes role="Admin", so signed-in customers get a 403 message instead.
Real-world use case#
An HR portal guards /payslips for any employee, /approvals for managers and /settings for HR admins. Deep links from email notifications land users on the right page after they sign in.
Common mistakes#
If the session check is still running, user is null and the guard redirects signed-in users. Render a loading state first.
Always sending users to “/” after login is annoying. Pass and use state.from.
Anyone can call the API directly. Enforce roles on the server.
Best practices#
- Guard groups of routes with a layout route
- Return users to where they were going
- Handle checking, signed-out and forbidden separately
- Mirror server-side authorization
Remember this#
Allowed → <Outlet />. Not signed in → <Navigate to="/login" />.
state={{ from: location }} → back after login.
Guards are UX; the API is the lock.
Practice exercise#
Your task: Write a GuestOnly route that redirects signed-in users away from /login and /register to /account.
Need a hint?
Show solution
import { Navigate, Outlet } from 'react-router';
export function GuestOnly({ user }) {
if (user) return <Navigate to="/account" replace />;
return <Outlet />;
}
// Router: { element: <GuestOnly user={user} />, children: [
// { path: '/login', element: <LoginPage /> },
// { path: '/register', element: <RegisterPage /> },
// ] }
Mini quiz#
Which component performs the redirect in a route guard?
Want more? Practise all React MCQs · React interview questions
My notes#
Saved in this browser.