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.
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
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
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
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
// 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
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.
