Oct 16, 2025

Protected Routes in React: Complete Guide with Examples

Tags

In modern web applications, not all pages should be accessible to everyone. Some routes contain sensitive information, such as user dashboards, admin panels, or personal profiles, that should only be accessible to authenticated users. This is where protected routes come in. 


Protected routes (also called Private routes) ensure that only authorized users can access certain parts of your application, automatically redirecting unauthorized visitors to login pages or public areas.


react-protected-routes



What are Protected Routes?


Protected routes are React components that verify a user's authentication or authorisation before rendering a page. If the user doesn't meet the requirements, they're redirected to a different route, typically a login page.

Key characteristics:


  • Check authentication status before rendering
  • Redirect unauthorized users automatically
  • Work seamlessly with React Router
  • Can implement role-based access control

Protected Routes vs Public Routes


Protected Routes
Public Routes
Require authentication to access
Accessible to everyone
Example: Dashboard, Profile, Admin
Example: Home, About, Login
Check user credentials first
Render immediately
Redirect if unauthorized
Always available



Basic Protected Route Implementation


Here's a simple protected route component using React Router v6:


import React from 'react';
import { Navigate, Outlet } from 'react-router-dom';

const ProtectedRoute = ({ isAuthenticated }) => {
  if (!isAuthenticated) {
    return <Navigate to="/login" replace />;
  }
  
  return <Outlet />;
};

export default ProtectedRoute;


Usage in your app:


import { BrowserRouter, Routes, Route } from 'react-router-dom';
import ProtectedRoute from './components/ProtectedRoute';

function App() {
  const [isAuthenticated, setIsAuthenticated] = useState(false);
  
  return (
    <BrowserRouter>
      <Routes>
        <Route path="/login" element={<Login />} />
        <Route path="/" element={<Home />} />
        
        {/* Protected routes */}
        <Route element={<ProtectedRoute isAuthenticated={isAuthenticated} />}>
          <Route path="/dashboard" element={<Dashboard />} />
          <Route path="/profile" element={<Profile />} />
        </Route>
      </Routes>
    </BrowserRouter>
  );
}



JWT and LocalStorage Implementation


For real applications, you'll typically use JWT tokens stored in localStorage:


const ProtectedRoute = ({ children }) => {
  const token = localStorage.getItem('authToken');
  
  if (!token) {
    return <Navigate to="/login" replace />;
  }
  
  // Optional: Verify token validity
  try {
    const decoded = jwt.decode(token);
    if (decoded.exp < Date.now() / 1000) {
      localStorage.removeItem('authToken');
      return <Navigate to="/login" replace />;
    }
  } catch (error) {
    return <Navigate to="/login" replace />;
  }
  
  return children;
};


Role-Based Protected Routes


For admin panels or role-specific access:


const RoleProtectedRoute = ({ children, requiredRole }) => {
  const user = JSON.parse(localStorage.getItem('user') || '{}');
  
  if (!user.token) {
    return <Navigate to="/login" replace />;
  }
  
  if (requiredRole && user.role !== requiredRole) {
    return <Navigate to="/unauthorized" replace />;
  }
  
  return children;
};

// Usage
<Route path="/admin" element={
  <RoleProtectedRoute requiredRole="admin">
    <AdminPanel />
  </RoleProtectedRoute>
} />


Context API Implementation


Using React Context for global authentication state:


// AuthContext.js
import React, { createContext, useContext, useState } from 'react';

const AuthContext = createContext();

export const useAuth = () => useContext(AuthContext);

export const AuthProvider = ({ children }) => {
  const [user, setUser] = useState(null);
  const [loading, setLoading] = useState(true);
  
  const login = async (credentials) => {
    // Login logic
    setUser(userData);
  };
  
  const logout = () => {
    localStorage.removeItem('authToken');
    setUser(null);
  };
  
  return (
    <AuthContext.Provider value={{ user, login, logout, loading }}>
      {children}
    </AuthContext.Provider>
  );
};

// ProtectedRoute with Context
const ProtectedRoute = ({ children }) => {
  const { user, loading } = useAuth();
  
  if (loading) {
    return <div>Loading...</div>;
  }
  
  return user ? children : <Navigate to="/login" replace />;
};


Reusable ProtectedRoute Component


A flexible, reusable component:


const ProtectedRoute = ({ 
  children, 
  redirectPath = '/login',
  requiredRole = null,
  fallback = null 
}) => {
  const { user, loading } = useAuth();
  
  if (loading) {
    return fallback || <div>Loading...</div>;
  }
  
  if (!user) {
    return <Navigate to={redirectPath} replace />;
  }
  
  if (requiredRole && user.role !== requiredRole) {
    return <Navigate to="/unauthorized" replace />;
  }
  
  return children;
};


Firebase Authentication Integration


Working with Firebase Auth:


import { useAuthState } from 'react-firebase-hooks/auth';
import { auth } from './firebase-config';

const ProtectedRoute = ({ children }) => {
  const [user, loading] = useAuthState(auth);
  
  if (loading) return <div>Loading...</div>;
  
  return user ? children : <Navigate to="/login" replace />;
};


Common Mistakes to Avoid


Not handling loading states: Always show loading indicators while checking authentication

Client-side only security: Remember that client-side route protection is UX, not security—always validate on the server

Token storage in localStorage: Consider security implications; httpOnly cookies might be safer

Not clearing expired tokens: Implement proper token validation and cleanup

Hard-coding redirect paths: Make redirect destinations configurable



// ❌ Wrong - No loading state
const ProtectedRoute = ({ children }) => {
  const user = getCurrentUser(); // This might take time
  return user ? children : <Navigate to="/login" />;
};

// ✅ Correct - Handle loading
const ProtectedRoute = ({ children }) => {
  const { user, loading } = useAuth();
  
  if (loading) return <div>Loading...</div>;
  return user ? children : <Navigate to="/login" replace />;
};


Summary


Protected routes are essential for building secure React applications with proper access control. They work by checking authentication status before rendering components and redirecting unauthorized users appropriately.


Whether you're using simple token-based authentication, role-based access control, or third-party services like Firebase, the patterns remain consistent: check credentials, handle loading states, and redirect when necessary.


Remember that client-side protection is primarily for user experience; always implement proper server-side validation for true security.



EmoticonEmoticon