dev.log / syntax diaries

Practical code notes, tools, and guided learning for developers.

Practical guides, developer tools, and tutorials for modern web developers, with the same focused tone across writing, utilities, and learning tracks.

BlogToolsTutorialsAboutContactAdmin Login
Privacy PolicyTerms of ServiceCookie Policy

ยฉ 2026 The Syntax Diaries ยท System_Operational

The Syntax Diaries logoThe Syntax Diaries
BlogToolsTutorialsAbout
build log live
Tutorial / React
Routing45 minbeginner

Protected Routes

Learn how to protect routes with authentication and authorization, creating secure navigation patterns in your React applications.

On This Page

The Problem: Open AccessWhat Are Protected Routes?Building Authentication ContextYour First Protected RouteBuilding a Complete Authentication SystemSetting Up Protected Routes in Your AppAdvanced Protection Patterns1. Permission-Based Routes2. Conditional Route Protection3. Route Guards with Loading StatesPractice Exercise: Multi-Role DashboardSecurity Best Practices1. Never Trust Frontend-Only Protection2. Implement Token Refresh3. Handle Concurrent Auth ChecksWhat We've LearnedQuick Recap QuizWhat's Next?

Protected Routes#

Imagine you're building a house with different rooms. Some rooms like the living room are open to everyone, but others like your bedroom or safe need a key to enter. Protected routes work the same way in web applications - they ensure only authorized users can access certain pages.

Without protected routes, anyone could type /admin or /user-profile into their browser and access sensitive areas of your app. Protected routes are your app's security guards, checking credentials before allowing entry.

The Problem: Open Access#

By default, all React Router routes are accessible to everyone:

function App() {
  return (
    <BrowserRouter>
      <Routes>
        <Route path="/" element={<HomePage />} />
        <Route path="/login" element={<LoginPage />} />
        <Route path="/dashboard" element={<Dashboard />} />     {/* Anyone can access! */}
        <Route path="/admin" element={<AdminPanel />} />        {/* Anyone can access! */}
        <Route path="/profile" element={<UserProfile />} />     {/* Anyone can access! */}
      </Routes>
    </BrowserRouter>
  );
}

Security problems:

  • ๐Ÿšจ Unauthorized access - Anyone can visit sensitive pages
  • ๐Ÿšจ Data exposure - Personal information visible to anyone
  • ๐Ÿšจ Admin functions exposed - Critical operations accessible to all
  • ๐Ÿšจ Poor user experience - Users see errors instead of proper login prompts

What Are Protected Routes?#

Protected routes are React components that:

  • Check user authentication before rendering content
  • Redirect to login if user is not authenticated
  • Verify permissions for role-based access
  • Provide smooth user experience with proper loading and error states

Think of them as smart bouncers for your app!

Building Authentication Context#

First, let's create a system to manage user authentication state:

import React, { createContext, useContext, useState, useEffect } from 'react';

// Create authentication context
const AuthContext = createContext(null);

// Custom hook to use auth context
export const useAuth = () => {
  const context = useContext(AuthContext);
  if (!context) {
    throw new Error('useAuth must be used within an AuthProvider');
  }
  return context;
};

// Authentication provider component
export function AuthProvider({ children }) {
  const [user, setUser] = useState(null);
  const [loading, setLoading] = useState(true);
  
  // Check if user is logged in on app start
  useEffect(() => {
    const checkAuthStatus = async () => {
      try {
        // Check localStorage for saved auth token
        const token = localStorage.getItem('authToken');
        if (token) {
          // Verify token with server (simplified for demo)
          const userData = await verifyToken(token);
          setUser(userData);
        }
      } catch (error) {
        // Token invalid, clear it
        localStorage.removeItem('authToken');
        console.error('Auth check failed:', error);
      } finally {
        setLoading(false);
      }
    };
    
    checkAuthStatus();
  }, []);
  
  // Login function
  const login = async (credentials) => {
    try {
      setLoading(true);
      
      // Call login API (simplified for demo)
      const response = await fetch('/api/login', {
        method: 'POST',
        headers: { 'Content-Type': 'application/json' },
        body: JSON.stringify(credentials)
      });
      
      if (!response.ok) {
        throw new Error('Login failed');
      }
      
      const { user: userData, token } = await response.json();
      
      // Store token and user data
      localStorage.setItem('authToken', token);
      setUser(userData);
      
      return { success: true };
    } catch (error) {
      return { success: false, error: error.message };
    } finally {
      setLoading(false);
    }
  };
  
  // Logout function
  const logout = async () => {
    try {
      // Call logout API to invalidate token
      await fetch('/api/logout', {
        method: 'POST',
        headers: {
          'Authorization': `Bearer \${localStorage.getItem('authToken')}`
        }
      });
    } catch (error) {
      console.error('Logout API failed:', error);
    } finally {
      // Always clear local state
      localStorage.removeItem('authToken');
      setUser(null);
    }
  };
  
  // Helper functions
  const isAuthenticated = () => !!user;
  const hasRole = (role) => user?.roles?.includes(role) || false;
  const hasPermission = (permission) => user?.permissions?.includes(permission) || false;
  
  const value = {
    user,
    loading,
    login,
    logout,
    isAuthenticated,
    hasRole,
    hasPermission
  };
  
  return (
    <AuthContext.Provider value={value}>
      {children}
    </AuthContext.Provider>
  );
}

// Simplified token verification (replace with real API call)
async function verifyToken(token) {
  // Simulate API call
  await new Promise(resolve => setTimeout(resolve, 1000));
  
  // Mock user data (replace with real API response)
  return {
    id: 1,
    name: 'John Doe',
    email: 'john@example.com',
    roles: ['user'],
    permissions: ['read:profile', 'write:profile']
  };
}

Your First Protected Route#

Now let's create a component that protects routes:

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

function ProtectedRoute({ children, requireAuth = true, requiredRole = null, requiredPermission = null }) {
  const { user, loading, isAuthenticated, hasRole, hasPermission } = useAuth();
  const location = useLocation();
  
  // Show loading spinner while checking authentication
  if (loading) {
    return (
      <div style={{
        display: 'flex',
        justifyContent: 'center',
        alignItems: 'center',
        height: '50vh',
        fontSize: '18px'
      }}>
        ๐Ÿ”„ Checking authentication...
      </div>
    );
  }
  
  // Check authentication requirement
  if (requireAuth && !isAuthenticated()) {
    // Redirect to login with return URL
    return <Navigate to="/login" state={{ from: location.pathname }} replace />;
  }
  
  // Check role requirement
  if (requiredRole && !hasRole(requiredRole)) {
    return (
      <div style={{
        textAlign: 'center',
        padding: '50px',
        backgroundColor: '#ffebee',
        borderRadius: '8px',
        margin: '20px'
      }}>
        <h2>๐Ÿšซ Access Denied</h2>
        <p>You don't have the required role ({requiredRole}) to access this page.</p>
        <p>Current roles: {user?.roles?.join(', ') || 'None'}</p>
      </div>
    );
  }
  
  // Check permission requirement
  if (requiredPermission && !hasPermission(requiredPermission)) {
    return (
      <div style={{
        textAlign: 'center',
        padding: '50px',
        backgroundColor: '#ffebee',
        borderRadius: '8px',
        margin: '20px'
      }}>
        <h2>๐Ÿšซ Insufficient Permissions</h2>
        <p>You don't have permission ({requiredPermission}) to access this page.</p>
        <p>Current permissions: {user?.permissions?.join(', ') || 'None'}</p>
      </div>
    );
  }
  
  // User is authorized, render the protected content
  return children;
}

export default ProtectedRoute;

Building a Complete Authentication System#

Let's create a full authentication system with login, registration, and protected pages:

import React, { useState } from 'react';
import { useAuth } from './AuthProvider';
import { useNavigate, useLocation, Link } from 'react-router-dom';

// Login Page Component
function LoginPage() {
  const [credentials, setCredentials] = useState({
    email: '',
    password: ''
  });
  const [error, setError] = useState('');
  const [isLoading, setIsLoading] = useState(false);
  
  const { login } = useAuth();
  const navigate = useNavigate();
  const location = useLocation();
  
  // Get the page user was trying to access
  const from = location.state?.from || '/dashboard';
  
  const handleSubmit = async (e) => {
    e.preventDefault();
    setError('');
    setIsLoading(true);
    
    const result = await login(credentials);
    
    if (result.success) {
      // Redirect to intended page or dashboard
      navigate(from, { replace: true });
    } else {
      setError(result.error);
    }
    
    setIsLoading(false);
  };
  
  const handleChange = (e) => {
    setCredentials({
      ...credentials,
      [e.target.name]: e.target.value
    });
  };
  
  return (
    <div style={{
      maxWidth: '400px',
      margin: '50px auto',
      padding: '30px',
      border: '1px solid #ddd',
      borderRadius: '8px',
      backgroundColor: '#fff'
    }}>
      <h1 style={{ textAlign: 'center', marginBottom: '30px' }}>
        ๐Ÿ” Login
      </h1>
      
      {location.state?.from && (
        <div style={{
          backgroundColor: '#fff3cd',
          color: '#856404',
          padding: '10px',
          borderRadius: '4px',
          marginBottom: '20px',
          textAlign: 'center'
        }}>
          Please log in to access {location.state.from}
        </div>
      )}
      
      {error && (
        <div style={{
          backgroundColor: '#ffebee',
          color: '#c62828',
          padding: '10px',
          borderRadius: '4px',
          marginBottom: '20px'
        }}>
          โŒ {error}
        </div>
      )}
      
      <form onSubmit={handleSubmit}>
        <div style={{ marginBottom: '20px' }}>
          <label style={{ display: 'block', marginBottom: '5px' }}>
            Email
          </label>
          <input
            type="email"
            name="email"
            value={credentials.email}
            onChange={handleChange}
            required
            style={{
              width: '100%',
              padding: '10px',
              border: '1px solid #ddd',
              borderRadius: '4px',
              fontSize: '16px'
            }}
            placeholder="Enter your email"
          />
        </div>
        
        <div style={{ marginBottom: '20px' }}>
          <label style={{ display: 'block', marginBottom: '5px' }}>
            Password
          </label>
          <input
            type="password"
            name="password"
            value={credentials.password}
            onChange={handleChange}
            required
            style={{
              width: '100%',
              padding: '10px',
              border: '1px solid #ddd',
              borderRadius: '4px',
              fontSize: '16px'
            }}
            placeholder="Enter your password"
          />
        </div>
        
        <button
          type="submit"
          disabled={isLoading}
          style={{
            width: '100%',
            padding: '12px',
            backgroundColor: isLoading ? '#ccc' : '#0066cc',
            color: 'white',
            border: 'none',
            borderRadius: '4px',
            fontSize: '16px',
            cursor: isLoading ? 'not-allowed' : 'pointer'
          }}
        >
          {isLoading ? 'Logging in...' : 'Login'}
        </button>
      </form>
      
      <div style={{ marginTop: '20px', textAlign: 'center' }}>
        <p>Don't have an account?</p>
        <Link to="/register" style={{ color: '#0066cc' }}>
          Create one here
        </Link>
      </div>
      
      {/* Demo credentials */}
      <div style={{
        marginTop: '30px',
        padding: '15px',
        backgroundColor: '#f8f9fa',
        borderRadius: '4px',
        fontSize: '14px'
      }}>
        <strong>Demo Credentials:</strong>
        <br />
        Email: user@demo.com | Password: password (User role)
        <br />
        Email: admin@demo.com | Password: admin123 (Admin role)
      </div>
    </div>
  );
}

// User Dashboard (Protected)
function UserDashboard() {
  const { user, logout } = useAuth();
  const navigate = useNavigate();
  
  const handleLogout = async () => {
    await logout();
    navigate('/');
  };
  
  return (
    <div style={{ padding: '20px' }}>
      <header style={{
        display: 'flex',
        justifyContent: 'space-between',
        alignItems: 'center',
        marginBottom: '30px',
        paddingBottom: '20px',
        borderBottom: '1px solid #eee'
      }}>
        <div>
          <h1>๐Ÿ‘ค User Dashboard</h1>
          <p>Welcome back, {user?.name}!</p>
        </div>
        
        <button
          onClick={handleLogout}
          style={{
            padding: '10px 20px',
            backgroundColor: '#dc3545',
            color: 'white',
            border: 'none',
            borderRadius: '4px',
            cursor: 'pointer'
          }}
        >
          Logout
        </button>
      </header>
      
      <div style={{
        display: 'grid',
        gridTemplateColumns: 'repeat(auto-fit, minmax(250px, 1fr))',
        gap: '20px'
      }}>
        <div style={{
          padding: '20px',
          border: '1px solid #ddd',
          borderRadius: '8px',
          backgroundColor: '#f8f9fa'
        }}>
          <h3>๐Ÿ“Š Account Overview</h3>
          <p><strong>Email:</strong> {user?.email}</p>
          <p><strong>Role:</strong> {user?.roles?.join(', ')}</p>
          <p><strong>Member since:</strong> January 2024</p>
        </div>
        
        <div style={{
          padding: '20px',
          border: '1px solid #ddd',
          borderRadius: '8px',
          backgroundColor: '#f8f9fa'
        }}>
          <h3>๐ŸŽฏ Quick Actions</h3>
          <div style={{ display: 'flex', flexDirection: 'column', gap: '10px' }}>
            <Link to="/profile" style={{
              padding: '8px 16px',
              backgroundColor: '#0066cc',
              color: 'white',
              textDecoration: 'none',
              borderRadius: '4px',
              textAlign: 'center'
            }}>
              Edit Profile
            </Link>
            <Link to="/settings" style={{
              padding: '8px 16px',
              backgroundColor: '#28a745',
              color: 'white',
              textDecoration: 'none',
              borderRadius: '4px',
              textAlign: 'center'
            }}>
              Settings
            </Link>
          </div>
        </div>
        
        <div style={{
          padding: '20px',
          border: '1px solid #ddd',
          borderRadius: '8px',
          backgroundColor: '#f8f9fa'
        }}>
          <h3>๐Ÿ“ˆ Recent Activity</h3>
          <ul style={{ listStyle: 'none', padding: 0 }}>
            <li style={{ padding: '5px 0', borderBottom: '1px solid #eee' }}>
              โœ… Profile updated
            </li>
            <li style={{ padding: '5px 0', borderBottom: '1px solid #eee' }}>
              ๐Ÿ“ New post created
            </li>
            <li style={{ padding: '5px 0' }}>
              ๐Ÿ”‘ Password changed
            </li>
          </ul>
        </div>
      </div>
    </div>
  );
}

// Admin Panel (Protected - Admin Only)
function AdminPanel() {
  const { user, logout } = useAuth();
  const navigate = useNavigate();
  const [users, setUsers] = useState([
    { id: 1, name: 'John Doe', email: 'john@demo.com', role: 'user', status: 'active' },
    { id: 2, name: 'Jane Smith', email: 'jane@demo.com', role: 'user', status: 'active' },
    { id: 3, name: 'Admin User', email: 'admin@demo.com', role: 'admin', status: 'active' }
  ]);
  
  const handleLogout = async () => {
    await logout();
    navigate('/');
  };
  
  const toggleUserStatus = (userId) => {
    setUsers(prev => prev.map(user => 
      user.id === userId 
        ? { ...user, status: user.status === 'active' ? 'suspended' : 'active' }
        : user
    ));
  };
  
  return (
    <div style={{ padding: '20px' }}>
      <header style={{
        display: 'flex',
        justifyContent: 'space-between',
        alignItems: 'center',
        marginBottom: '30px',
        paddingBottom: '20px',
        borderBottom: '1px solid #eee'
      }}>
        <div>
          <h1>โšก Admin Panel</h1>
          <p>Welcome, {user?.name} (Administrator)</p>
        </div>
        
        <div style={{ display: 'flex', gap: '10px' }}>
          <Link
            to="/dashboard"
            style={{
              padding: '10px 20px',
              backgroundColor: '#6c757d',
              color: 'white',
              textDecoration: 'none',
              borderRadius: '4px'
            }}
          >
            User Dashboard
          </Link>
          <button
            onClick={handleLogout}
            style={{
              padding: '10px 20px',
              backgroundColor: '#dc3545',
              color: 'white',
              border: 'none',
              borderRadius: '4px',
              cursor: 'pointer'
            }}
          >
            Logout
          </button>
        </div>
      </header>
      
      <div style={{ marginBottom: '30px' }}>
        <h2>๐Ÿ“Š System Overview</h2>
        <div style={{
          display: 'grid',
          gridTemplateColumns: 'repeat(auto-fit, minmax(200px, 1fr))',
          gap: '20px',
          marginTop: '20px'
        }}>
          {[
            { label: 'Total Users', value: users.length, color: '#0066cc' },
            { label: 'Active Users', value: users.filter(u => u.status === 'active').length, color: '#28a745' },
            { label: 'Admins', value: users.filter(u => u.role === 'admin').length, color: '#ffc107' },
            { label: 'Suspended', value: users.filter(u => u.status === 'suspended').length, color: '#dc3545' }
          ].map((stat, index) => (
            <div key={index} style={{
              padding: '20px',
              backgroundColor: stat.color,
              color: 'white',
              borderRadius: '8px',
              textAlign: 'center'
            }}>
              <h3 style={{ margin: '0 0 10px 0', fontSize: '2em' }}>{stat.value}</h3>
              <p style={{ margin: 0 }}>{stat.label}</p>
            </div>
          ))}
        </div>
      </div>
      
      <div>
        <h2>๐Ÿ‘ฅ User Management</h2>
        <div style={{ marginTop: '20px' }}>
          <table style={{
            width: '100%',
            borderCollapse: 'collapse',
            backgroundColor: 'white',
            borderRadius: '8px',
            overflow: 'hidden',
            boxShadow: '0 2px 4px rgba(0,0,0,0.1)'
          }}>
            <thead>
              <tr style={{ backgroundColor: '#f8f9fa' }}>
                <th style={{ padding: '15px', textAlign: 'left', borderBottom: '1px solid #dee2e6' }}>Name</th>
                <th style={{ padding: '15px', textAlign: 'left', borderBottom: '1px solid #dee2e6' }}>Email</th>
                <th style={{ padding: '15px', textAlign: 'left', borderBottom: '1px solid #dee2e6' }}>Role</th>
                <th style={{ padding: '15px', textAlign: 'left', borderBottom: '1px solid #dee2e6' }}>Status</th>
                <th style={{ padding: '15px', textAlign: 'left', borderBottom: '1px solid #dee2e6' }}>Actions</th>
              </tr>
            </thead>
            <tbody>
              {users.map(user => (
                <tr key={user.id}>
                  <td style={{ padding: '15px', borderBottom: '1px solid #dee2e6' }}>{user.name}</td>
                  <td style={{ padding: '15px', borderBottom: '1px solid #dee2e6' }}>{user.email}</td>
                  <td style={{ padding: '15px', borderBottom: '1px solid #dee2e6' }}>
                    <span style={{
                      padding: '4px 8px',
                      borderRadius: '12px',
                      fontSize: '12px',
                      backgroundColor: user.role === 'admin' ? '#ffc107' : '#0066cc',
                      color: 'white'
                    }}>
                      {user.role}
                    </span>
                  </td>
                  <td style={{ padding: '15px', borderBottom: '1px solid #dee2e6' }}>
                    <span style={{
                      padding: '4px 8px',
                      borderRadius: '12px',
                      fontSize: '12px',
                      backgroundColor: user.status === 'active' ? '#28a745' : '#dc3545',
                      color: 'white'
                    }}>
                      {user.status}
                    </span>
                  </td>
                  <td style={{ padding: '15px', borderBottom: '1px solid #dee2e6' }}>
                    <button
                      onClick={() => toggleUserStatus(user.id)}
                      style={{
                        padding: '6px 12px',
                        backgroundColor: user.status === 'active' ? '#dc3545' : '#28a745',
                        color: 'white',
                        border: 'none',
                        borderRadius: '4px',
                        cursor: 'pointer',
                        fontSize: '12px'
                      }}
                    >
                      {user.status === 'active' ? 'Suspend' : 'Activate'}
                    </button>
                  </td>
                </tr>
              ))}
            </tbody>
          </table>
        </div>
      </div>
    </div>
  );
}

// User Profile Page (Protected)
function UserProfile() {
  const { user } = useAuth();
  const [profile, setProfile] = useState({
    name: user?.name || '',
    email: user?.email || '',
    bio: 'I love building amazing web applications with React!',
    phone: '+1 (555) 123-4567',
    location: 'San Francisco, CA'
  });
  const [isEditing, setIsEditing] = useState(false);
  
  const handleSave = () => {
    // In a real app, you'd save to a backend
    alert('Profile updated successfully!');
    setIsEditing(false);
  };
  
  const handleChange = (e) => {
    setProfile({
      ...profile,
      [e.target.name]: e.target.value
    });
  };
  
  return (
    <div style={{ padding: '20px', maxWidth: '600px', margin: '0 auto' }}>
      <div style={{
        display: 'flex',
        justifyContent: 'space-between',
        alignItems: 'center',
        marginBottom: '30px'
      }}>
        <h1>๐Ÿ‘ค User Profile</h1>
        <button
          onClick={() => setIsEditing(!isEditing)}
          style={{
            padding: '10px 20px',
            backgroundColor: isEditing ? '#6c757d' : '#0066cc',
            color: 'white',
            border: 'none',
            borderRadius: '4px',
            cursor: 'pointer'
          }}
        >
          {isEditing ? 'Cancel' : 'Edit Profile'}
        </button>
      </div>
      
      <div style={{
        backgroundColor: 'white',
        padding: '30px',
        borderRadius: '8px',
        border: '1px solid #ddd'
      }}>
        <div style={{ textAlign: 'center', marginBottom: '30px' }}>
          <div style={{
            width: '100px',
            height: '100px',
            borderRadius: '50%',
            backgroundColor: '#0066cc',
            color: 'white',
            display: 'flex',
            alignItems: 'center',
            justifyContent: 'center',
            fontSize: '36px',
            margin: '0 auto 15px'
          }}>
            {profile.name.charAt(0).toUpperCase()}
          </div>
          <h2 style={{ margin: '0 0 5px 0' }}>{profile.name}</h2>
          <p style={{ color: '#666', margin: 0 }}>{profile.email}</p>
        </div>
        
        <form>
          <div style={{ marginBottom: '20px' }}>
            <label style={{ display: 'block', marginBottom: '5px', fontWeight: 'bold' }}>
              Full Name
            </label>
            <input
              type="text"
              name="name"
              value={profile.name}
              onChange={handleChange}
              disabled={!isEditing}
              style={{
                width: '100%',
                padding: '10px',
                border: '1px solid #ddd',
                borderRadius: '4px',
                backgroundColor: isEditing ? 'white' : '#f8f9fa'
              }}
            />
          </div>
          
          <div style={{ marginBottom: '20px' }}>
            <label style={{ display: 'block', marginBottom: '5px', fontWeight: 'bold' }}>
              Email Address
            </label>
            <input
              type="email"
              name="email"
              value={profile.email}
              onChange={handleChange}
              disabled={!isEditing}
              style={{
                width: '100%',
                padding: '10px',
                border: '1px solid #ddd',
                borderRadius: '4px',
                backgroundColor: isEditing ? 'white' : '#f8f9fa'
              }}
            />
          </div>
          
          <div style={{ marginBottom: '20px' }}>
            <label style={{ display: 'block', marginBottom: '5px', fontWeight: 'bold' }}>
              Phone Number
            </label>
            <input
              type="tel"
              name="phone"
              value={profile.phone}
              onChange={handleChange}
              disabled={!isEditing}
              style={{
                width: '100%',
                padding: '10px',
                border: '1px solid #ddd',
                borderRadius: '4px',
                backgroundColor: isEditing ? 'white' : '#f8f9fa'
              }}
            />
          </div>
          
          <div style={{ marginBottom: '20px' }}>
            <label style={{ display: 'block', marginBottom: '5px', fontWeight: 'bold' }}>
              Location
            </label>
            <input
              type="text"
              name="location"
              value={profile.location}
              onChange={handleChange}
              disabled={!isEditing}
              style={{
                width: '100%',
                padding: '10px',
                border: '1px solid #ddd',
                borderRadius: '4px',
                backgroundColor: isEditing ? 'white' : '#f8f9fa'
              }}
            />
          </div>
          
          <div style={{ marginBottom: '20px' }}>
            <label style={{ display: 'block', marginBottom: '5px', fontWeight: 'bold' }}>
              Bio
            </label>
            <textarea
              name="bio"
              value={profile.bio}
              onChange={handleChange}
              disabled={!isEditing}
              rows={4}
              style={{
                width: '100%',
                padding: '10px',
                border: '1px solid #ddd',
                borderRadius: '4px',
                backgroundColor: isEditing ? 'white' : '#f8f9fa',
                resize: 'vertical'
              }}
            />
          </div>
          
          {isEditing && (
            <button
              type="button"
              onClick={handleSave}
              style={{
                width: '100%',
                padding: '12px',
                backgroundColor: '#28a745',
                color: 'white',
                border: 'none',
                borderRadius: '4px',
                cursor: 'pointer',
                fontSize: '16px'
              }}
            >
              Save Changes
            </button>
          )}
        </form>
      </div>
    </div>
  );
}

Setting Up Protected Routes in Your App#

Now let's put it all together in your main App component:

import React from 'react';
import { BrowserRouter, Routes, Route, Link } from 'react-router-dom';
import { AuthProvider, useAuth } from './AuthProvider';
import ProtectedRoute from './ProtectedRoute';

// Navigation component that shows different options based on auth state
function Navigation() {
  const { user, isAuthenticated, logout } = useAuth();
  
  const handleLogout = async () => {
    await logout();
  };
  
  return (
    <nav style={{
      backgroundColor: '#333',
      padding: '1rem 0',
      marginBottom: '20px'
    }}>
      <div style={{
        maxWidth: '1200px',
        margin: '0 auto',
        display: 'flex',
        justifyContent: 'space-between',
        alignItems: 'center',
        padding: '0 20px'
      }}>
        <Link 
          to="/" 
          style={{ 
            color: 'white', 
            textDecoration: 'none', 
            fontSize: '24px', 
            fontWeight: 'bold' 
          }}
        >
          SecureApp
        </Link>
        
        <div style={{ display: 'flex', gap: '20px', alignItems: 'center' }}>
          <Link to="/" style={{ color: 'white', textDecoration: 'none' }}>
            Home
          </Link>
          
          {isAuthenticated() ? (
            <>
              <Link to="/dashboard" style={{ color: 'white', textDecoration: 'none' }}>
                Dashboard
              </Link>
              <Link to="/profile" style={{ color: 'white', textDecoration: 'none' }}>
                Profile
              </Link>
              
              {/* Show admin link only for admin users */}
              {user?.roles?.includes('admin') && (
                <Link to="/admin" style={{ color: 'white', textDecoration: 'none' }}>
                  Admin Panel
                </Link>
              )}
              
              <div style={{ display: 'flex', alignItems: 'center', gap: '15px' }}>
                <span style={{ color: 'white' }}>
                  Welcome, {user?.name}
                </span>
                <button
                  onClick={handleLogout}
                  style={{
                    padding: '8px 16px',
                    backgroundColor: '#dc3545',
                    color: 'white',
                    border: 'none',
                    borderRadius: '4px',
                    cursor: 'pointer'
                  }}
                >
                  Logout
                </button>
              </div>
            </>
          ) : (
            <>
              <Link to="/login" style={{ color: 'white', textDecoration: 'none' }}>
                Login
              </Link>
              <Link 
                to="/register" 
                style={{
                  color: 'white',
                  textDecoration: 'none',
                  padding: '8px 16px',
                  backgroundColor: '#0066cc',
                  borderRadius: '4px'
                }}
              >
                Sign Up
              </Link>
            </>
          )}
        </div>
      </div>
    </nav>
  );
}

// Home page (public)
function HomePage() {
  const { isAuthenticated, user } = useAuth();
  
  return (
    <div style={{ padding: '20px', textAlign: 'center' }}>
      <h1>๐Ÿ  Welcome to SecureApp</h1>
      <p style={{ fontSize: '18px', marginBottom: '30px' }}>
        A demonstration of protected routes and authentication in React.
      </p>
      
      {isAuthenticated() ? (
        <div>
          <p>Welcome back, {user?.name}!</p>
          <div style={{ display: 'flex', gap: '15px', justifyContent: 'center' }}>
            <Link 
              to="/dashboard"
              style={{
                padding: '12px 24px',
                backgroundColor: '#0066cc',
                color: 'white',
                textDecoration: 'none',
                borderRadius: '4px'
              }}
            >
              Go to Dashboard
            </Link>
            <Link 
              to="/profile"
              style={{
                padding: '12px 24px',
                backgroundColor: '#28a745',
                color: 'white',
                textDecoration: 'none',
                borderRadius: '4px'
              }}
            >
              View Profile
            </Link>
          </div>
        </div>
      ) : (
        <div>
          <p>Please log in to access protected features.</p>
          <div style={{ display: 'flex', gap: '15px', justifyContent: 'center' }}>
            <Link 
              to="/login"
              style={{
                padding: '12px 24px',
                backgroundColor: '#0066cc',
                color: 'white',
                textDecoration: 'none',
                borderRadius: '4px'
              }}
            >
              Login
            </Link>
            <Link 
              to="/register"
              style={{
                padding: '12px 24px',
                backgroundColor: '#28a745',
                color: 'white',
                textDecoration: 'none',
                borderRadius: '4px'
              }}
            >
              Sign Up
            </Link>
          </div>
        </div>
      )}
      
      <div style={{
        marginTop: '50px',
        padding: '30px',
        backgroundColor: '#f8f9fa',
        borderRadius: '8px',
        maxWidth: '800px',
        margin: '50px auto 0'
      }}>
        <h2>๐Ÿ”’ Security Features</h2>
        <div style={{ 
          display: 'grid', 
          gridTemplateColumns: 'repeat(auto-fit, minmax(250px, 1fr))',
          gap: '20px',
          marginTop: '20px'
        }}>
          <div>
            <h3>โœ… Authentication</h3>
            <p>Secure login/logout with token management and session persistence.</p>
          </div>
          <div>
            <h3>๐Ÿ›ก๏ธ Route Protection</h3>
            <p>Automatically redirect unauthorized users to login page.</p>
          </div>
          <div>
            <h3>๐Ÿ‘‘ Role-Based Access</h3>
            <p>Different permissions for regular users and administrators.</p>
          </div>
        </div>
      </div>
    </div>
  );
}

// Registration page (public)
function RegisterPage() {
  const [formData, setFormData] = useState({
    name: '',
    email: '',
    password: '',
    confirmPassword: ''
  });
  const [error, setError] = useState('');
  const [success, setSuccess] = useState(false);
  
  const handleSubmit = (e) => {
    e.preventDefault();
    setError('');
    
    if (formData.password !== formData.confirmPassword) {
      setError('Passwords do not match');
      return;
    }
    
    // Simulate registration
    setSuccess(true);
  };
  
  const handleChange = (e) => {
    setFormData({
      ...formData,
      [e.target.name]: e.target.value
    });
  };
  
  if (success) {
    return (
      <div style={{
        maxWidth: '400px',
        margin: '50px auto',
        padding: '30px',
        textAlign: 'center',
        border: '1px solid #28a745',
        borderRadius: '8px',
        backgroundColor: '#d4edda'
      }}>
        <h2>โœ… Registration Successful!</h2>
        <p>Your account has been created. You can now log in.</p>
        <Link 
          to="/login"
          style={{
            padding: '12px 24px',
            backgroundColor: '#28a745',
            color: 'white',
            textDecoration: 'none',
            borderRadius: '4px',
            display: 'inline-block'
          }}
        >
          Go to Login
        </Link>
      </div>
    );
  }
  
  return (
    <div style={{
      maxWidth: '400px',
      margin: '50px auto',
      padding: '30px',
      border: '1px solid #ddd',
      borderRadius: '8px'
    }}>
      <h1 style={{ textAlign: 'center', marginBottom: '30px' }}>
        ๐Ÿ“ Create Account
      </h1>
      
      {error && (
        <div style={{
          backgroundColor: '#ffebee',
          color: '#c62828',
          padding: '10px',
          borderRadius: '4px',
          marginBottom: '20px'
        }}>
          โŒ {error}
        </div>
      )}
      
      <form onSubmit={handleSubmit}>
        <div style={{ marginBottom: '20px' }}>
          <label style={{ display: 'block', marginBottom: '5px' }}>
            Full Name
          </label>
          <input
            type="text"
            name="name"
            value={formData.name}
            onChange={handleChange}
            required
            style={{
              width: '100%',
              padding: '10px',
              border: '1px solid #ddd',
              borderRadius: '4px'
            }}
          />
        </div>
        
        <div style={{ marginBottom: '20px' }}>
          <label style={{ display: 'block', marginBottom: '5px' }}>
            Email Address
          </label>
          <input
            type="email"
            name="email"
            value={formData.email}
            onChange={handleChange}
            required
            style={{
              width: '100%',
              padding: '10px',
              border: '1px solid #ddd',
              borderRadius: '4px'
            }}
          />
        </div>
        
        <div style={{ marginBottom: '20px' }}>
          <label style={{ display: 'block', marginBottom: '5px' }}>
            Password
          </label>
          <input
            type="password"
            name="password"
            value={formData.password}
            onChange={handleChange}
            required
            style={{
              width: '100%',
              padding: '10px',
              border: '1px solid #ddd',
              borderRadius: '4px'
            }}
          />
        </div>
        
        <div style={{ marginBottom: '20px' }}>
          <label style={{ display: 'block', marginBottom: '5px' }}>
            Confirm Password
          </label>
          <input
            type="password"
            name="confirmPassword"
            value={formData.confirmPassword}
            onChange={handleChange}
            required
            style={{
              width: '100%',
              padding: '10px',
              border: '1px solid #ddd',
              borderRadius: '4px'
            }}
          />
        </div>
        
        <button
          type="submit"
          style={{
            width: '100%',
            padding: '12px',
            backgroundColor: '#0066cc',
            color: 'white',
            border: 'none',
            borderRadius: '4px',
            fontSize: '16px',
            cursor: 'pointer'
          }}
        >
          Create Account
        </button>
      </form>
      
      <div style={{ marginTop: '20px', textAlign: 'center' }}>
        <p>Already have an account?</p>
        <Link to="/login" style={{ color: '#0066cc' }}>
          Sign in here
        </Link>
      </div>
    </div>
  );
}

// Main App Component
function App() {
  return (
    <AuthProvider>
      <BrowserRouter>
        <div style={{ minHeight: '100vh', backgroundColor: '#f8f9fa' }}>
          <Navigation />
          
          <div style={{ maxWidth: '1200px', margin: '0 auto' }}>
            <Routes>
              {/* Public routes */}
              <Route path="/" element={<HomePage />} />
              <Route path="/login" element={<LoginPage />} />
              <Route path="/register" element={<RegisterPage />} />
              
              {/* Protected routes - require authentication */}
              <Route 
                path="/dashboard" 
                element={
                  <ProtectedRoute>
                    <UserDashboard />
                  </ProtectedRoute>
                } 
              />
              
              <Route 
                path="/profile" 
                element={
                  <ProtectedRoute>
                    <UserProfile />
                  </ProtectedRoute>
                } 
              />
              
              {/* Admin-only route */}
              <Route 
                path="/admin" 
                element={
                  <ProtectedRoute requiredRole="admin">
                    <AdminPanel />
                  </ProtectedRoute>
                } 
              />
              
              {/* 404 page */}
              <Route 
                path="*" 
                element={
                  <div style={{ textAlign: 'center', padding: '50px' }}>
                    <h1>404 - Page Not Found</h1>
                    <Link to="/">Go Home</Link>
                  </div>
                } 
              />
            </Routes>
          </div>
        </div>
      </BrowserRouter>
    </AuthProvider>
  );
}

export default App;

Advanced Protection Patterns#

1. Permission-Based Routes#

// Route that requires specific permissions
<Route 
  path="/edit-post/:postId" 
  element={
    <ProtectedRoute requiredPermission="edit:posts">
      <EditPost />
    </ProtectedRoute>
  } 
/>

// Component with granular permission checks
function PostEditor() {
  const { hasPermission } = useAuth();
  
  return (
    <div>
      <h1>Edit Post</h1>
      
      {hasPermission('publish:posts') && (
        <button>Publish Post</button>
      )}
      
      {hasPermission('delete:posts') && (
        <button>Delete Post</button>
      )}
    </div>
  );
}

2. Conditional Route Protection#

function ConditionalProtectedRoute({ children, condition, fallback }) {
  if (!condition) {
    return fallback || <Navigate to="/unauthorized" />;
  }
  
  return children;
}

// Usage: Protect based on subscription status
<Route 
  path="/premium-features" 
  element={
    <ConditionalProtectedRoute 
      condition={user?.subscription === 'premium'}
      fallback={<UpgradePrompt />}
    >
      <PremiumFeatures />
    </ConditionalProtectedRoute>
  } 
/>

3. Route Guards with Loading States#

function RouteGuard({ children, checkFunction, loadingComponent }) {
  const [isAllowed, setIsAllowed] = useState(null);
  
  useEffect(() => {
    checkFunction().then(setIsAllowed);
  }, [checkFunction]);
  
  if (isAllowed === null) {
    return loadingComponent || <div>Checking permissions...</div>;
  }
  
  if (!isAllowed) {
    return <Navigate to="/unauthorized" />;
  }
  
  return children;
}

Practice Exercise: Multi-Role Dashboard#

Build a dashboard with different views based on user roles:

Requirements:

  1. Guest users: Can only see public content and login prompt
  2. Regular users: Can access personal dashboard and profile
  3. Moderators: Can access user management features
  4. Admins: Can access everything including system settings

Starter structure:

const roleHierarchy = {
  guest: 0,
  user: 1,
  moderator: 2,
  admin: 3
};

function hasMinimumRole(userRole, requiredRole) {
  return roleHierarchy[userRole] >= roleHierarchy[requiredRole];
}

function RoleBasedRoute({ children, minimumRole = 'user' }) {
  // Your implementation here
}

// Usage
<Route 
  path="/moderate" 
  element={
    <RoleBasedRoute minimumRole="moderator">
      <ModerationPanel />
    </RoleBasedRoute>
  } 
/>

Security Best Practices#

1. Never Trust Frontend-Only Protection#

// โŒ Bad - only frontend protection
function AdminPanel() {
  const { user } = useAuth();
  
  if (user?.role !== 'admin') {
    return <div>Access denied</div>;
  }
  
  // Admin functionality - but API calls aren't protected!
  return <div>Delete all users button</div>;
}

// โœ… Good - backend also validates
function AdminPanel() {
  const { user } = useAuth();
  
  if (user?.role !== 'admin') {
    return <div>Access denied</div>;
  }
  
  const deleteUser = async (userId) => {
    // Backend will also check if user is admin
    const response = await fetch('/api/admin/users/' + userId, {
      method: 'DELETE',
      headers: { 
        'Authorization': 'Bearer ' + getToken(),
        'Content-Type': 'application/json'
      }
    });
    
    if (!response.ok) {
      throw new Error('Unauthorized or failed');
    }
  };
  
  return <div>{/* Admin functionality */}</div>;
}

2. Implement Token Refresh#

// Add token refresh logic to your auth context
const refreshToken = async () => {
  const refreshToken = localStorage.getItem('refreshToken');
  
  const response = await fetch('/api/refresh', {
    method: 'POST',
    headers: { 'Content-Type': 'application/json' },
    body: JSON.stringify({ refreshToken })
  });
  
  if (response.ok) {
    const { accessToken } = await response.json();
    localStorage.setItem('authToken', accessToken);
    return accessToken;
  }
  
  // Refresh failed, logout user
  logout();
  return null;
};

3. Handle Concurrent Auth Checks#

// Prevent multiple simultaneous auth checks
let authCheckPromise = null;

const checkAuthStatus = async () => {
  if (authCheckPromise) {
    return authCheckPromise;
  }
  
  authCheckPromise = performAuthCheck();
  const result = await authCheckPromise;
  authCheckPromise = null;
  
  return result;
};

What We've Learned#

Congratulations! You now understand:

โœ… What protected routes are and why they're essential for security
โœ… How to implement authentication context and state management
โœ… Building route guards that check permissions before rendering
โœ… Role-based and permission-based access control
โœ… Handling login/logout flows with proper redirects
โœ… Security best practices and common pitfalls to avoid

Quick Recap Quiz#

Test your protected routes knowledge:

  1. What should you do if a user tries to access a protected route without authentication?
  2. How do you pass the intended destination to the login page?
  3. What's the difference between role-based and permission-based access control?
  4. Why shouldn't you rely only on frontend route protection?

Answers: 1) Redirect to login page with return URL, 2) Use location state or query parameters, 3) Roles are broad categories, permissions are specific actions, 4) Backend must also validate - frontend is for UX only

What's Next?#

In our final lesson, we'll build a Complete React Project that combines everything you've learned! You'll create a full-featured application with components, state management, routing, authentication, and more. This capstone project will demonstrate your mastery of React development.

Protected routes provide the security foundation for building real-world applications!

Previous

What is React?

Next

React Router