Protected Routes

Implement protected routes in React — the pattern for restricting access to authenticated users and specific roles. Learn the correct implementation pattern used in production applications.

Last reviewed: January 2025

·React 16+React Router v6
✅
Quick Understanding: A protected route checks if the user is authenticated (or has the required role) before rendering the route. If not, it redirects to the login page. In React Router v6, this is implemented as a layout route component that wraps protected routes and performs the auth check.

🔍 What Is It?

A protected route is a route guard — a component that checks a condition before rendering its children. In React Router v6, the pattern uses a layout route:

function RequireAuth() {
  const { user, isLoading } = useAuth();
  const location = useLocation();

  if (isLoading) return <Spinner />;
  if (!user) return <Navigate to="/login" state={{ from: location }} replace />;
  return <Outlet />; // render the protected route
}

This component either renders <Outlet /> (the protected content) or redirects to login.

🎯 Why Do We Need It?

Without route protection:

  • →Unauthenticated users can access any URL directly
  • →Users with wrong roles can access admin pages
  • →Sensitive data is exposed to anyone who knows the URL

Protected routes enforce access control at the routing layer, before any data is fetched or rendered.

🏗️ What Happens in Real Projects?

Every authenticated app needs protected routes:

  • →Redirect unauthenticated users to /login
  • →After login, redirect back to the page they tried to access
  • →Admin-only routes that redirect non-admins to a 403 page
  • →Feature-flag routes that redirect users without the feature enabled

⚙️ How It Works

  1. Create a RequireAuth layout route component
  2. It reads auth state from your auth context/store
  3. If loading: show a spinner (prevents flash of redirect)
  4. If not authenticated: redirect to /login, preserving the intended destination in state
  5. If authenticated: render <Outlet /> (the protected content)
  6. After login, read the from location from state and redirect back

💻 Example

Protected Routes Implementation — Auth guard, role guard, and post-login redirect pattern.

tsx
import { Navigate, Outlet, useLocation } from 'react-router-dom';
import { useAuth } from '../hooks/useAuth';

// Auth guard — requires any authenticated user
export function RequireAuth() {
  const { user, isLoading } = useAuth();
  const location = useLocation();

  if (isLoading) return <FullPageSpinner />;

  if (!user) {
    // Preserve the intended destination for post-login redirect
    return <Navigate to="/login" state={{ from: location }} replace />;
  }

  return <Outlet />;
}

// Role guard — requires a specific role
interface RequireRoleProps { role: 'admin' | 'adjuster' | 'viewer'; }

export function RequireRole({ role }: RequireRoleProps) {
  const { user } = useAuth();

  if (!user || user.role !== role) {
    return <Navigate to="/403" replace />;
  }

  return <Outlet />;
}

// Login page — redirects back after successful login
export function LoginPage() {
  const { login } = useAuth();
  const navigate = useNavigate();
  const location = useLocation();
  const from = (location.state as { from?: Location })?.from?.pathname ?? '/dashboard';

  const handleLogin = async (email: string, password: string) => {
    await login(email, password);
    navigate(from, { replace: true }); // go back to where they came from
  };

  return <LoginForm onSubmit={handleLogin} />;
}

🤔 Common Confusions

❓ Why do I see a flash of the protected content before the redirect?

This happens when auth state initialises as null (unauthenticated) before the session is restored. Fix: add an isLoading state to your auth context. While isLoading is true, show a spinner. Only redirect when isLoading is false AND user is null.

❓ Should I protect routes on the frontend or backend?

Both. Frontend route protection is a UX concern — it prevents rendering the wrong UI and redirects users cleanly. Backend API protection is a security concern — it prevents unauthorised data access. Never rely solely on frontend route protection for security. Always validate auth on every API call.

🚨 Common Production Issues

**Flash of redirect** — Auth loads asynchronously. If you redirect immediately when user is null, you redirect before the session is restored. Always check isLoading before redirecting.

**Lost navigation state** — The from location state is lost if the user refreshes the login page. Handle the fallback: const from = location.state?.from?.pathname ?? "/dashboard".

**Security: Client-side only** — Route guards are a UX improvement, not a security boundary. Always validate tokens on every API request.

✅ Best Practices

Always handle the isLoading state to prevent flash of redirect

Preserve the intended destination in navigation state for post-login redirect

Create separate guards for auth (RequireAuth) and roles (RequireRole)

Never rely on frontend guards alone — always validate on the server

Provide a clear 403 Forbidden page for role violations

🏛️ Architect's Note

Senior Architect's Perspective

Protected routes are a UX layer, not a security layer. The real security is your API — every endpoint must validate the JWT and check permissions server-side. Frontend guards prevent the wrong UI from rendering and provide a good user experience. They are not a substitute for backend security. This distinction is important to communicate clearly to product and business stakeholders.

💬 Frequently Asked Questions

How do I test protected routes?

In tests, wrap the component with a mock AuthProvider that returns a specific user. Test both authenticated and unauthenticated states. For the redirect behaviour, check that the Navigate component renders with the correct to prop when user is null.

🎯 Key Takeaways

1Protected routes use a layout route component that checks auth before rendering Outlet
2Always handle isLoading to prevent flash of redirect on session restore
3Preserve the intended destination in state for post-login redirect
4Create separate guards for authentication and role checks
5Frontend route guards are UX — backend API validation is security

🎯 Practice this topic in interviews

↗ Where this topic fits

Sources

Sources / References

Use these React references to verify the concept in official docs and continue into the most relevant SplashRide pages.

Related SplashRide pages

SplashRide React learning path

Browse the full React topic collection and learning order on SplashRide.

React Interview Prep

Practice React interview questions and scenario-based answers.

Frontend Engineer Career Path

A beginner-friendly journey through web foundations, React, production frontend engineering, and interview preparation.

Python Developer Career Path

A practical route through Python fundamentals, automation, backend basics, testing, projects, interview preparation, and career growth.

React Router

Master React Router v6 — the standard routing library for React SPAs. Learn route definitions, navigation, URL params, and the patterns senior engineers use in production applications.

React Nested Routes with Outlet, Layouts, and Index Routes

Build nested routes in React Router with Outlet, shared layouts, relative child paths, and index routes. Includes a minimal example and fixes for blank child pages.