Protected Routes
Protected Routes in React Router
Protected routes are routes that require authentication or specific permissions to access. This pattern is essential for building secure applications with private areas like user dashboards, admin panels, or premium content.
Basic Protected Route
Simple Authentication Check
import { Navigate } from 'react-router-dom';
function ProtectedRoute({ children }) {
const isAuthenticated = localStorage.getItem('authToken');
if (!isAuthenticated) {
// Redirect to login if not authenticated
return <Navigate to="/login" replace />;
}
return children;
}
// Usage
function App() {
return (
<Routes>
<Route path="/login" element={<Login />} />
<Route
path="/dashboard"
element={
<ProtectedRoute>
<Dashboard />
</ProtectedRoute>
}
/>
</Routes>
);
}
With Authentication Context
// AuthContext.js
const AuthContext = createContext(null);
export function AuthProvider({ children }) {
const [user, setUser] = useState(null);
const [loading, setLoading] = useState(true);
useEffect(() => {
// Check if user is logged in
const token = localStorage.getItem('authToken');
if (token) {
fetchUser(token)
.then(setUser)
.catch(() => localStorage.removeItem('authToken'))
.finally(() => setLoading(false));
} else {
setLoading(false);
}
}, []);
const login = async (credentials) => {
const response = await fetch('/api/login', {
method: 'POST',
body: JSON.stringify(credentials)
});
if (response.ok) {
const { user, token } = await response.json();
localStorage.setItem('authToken', token);
setUser(user);
return { success: true };
}
return { success: false, error: 'Invalid credentials' };
};
const logout = () => {
localStorage.removeItem('authToken');
setUser(null);
};
return (
<AuthContext.Provider value={{ user, login, logout, loading }}>
{children}
</AuthContext.Provider>
);
}
export function useAuth() {
const context = useContext(AuthContext);
if (!context) {
throw new Error('useAuth must be used within AuthProvider');
}
return context;
}
// ProtectedRoute component
function ProtectedRoute({ children }) {
const { user, loading } = useAuth();
const location = useLocation();
if (loading) {
return <div>Loading...</div>;
}
if (!user) {
// Save the location they were trying to go to
return <Navigate to="/login" state={{ from: location }} replace />;
}
return children;
}
Role-Based Protection
Simple Role Check
function RequireRole({ children, allowedRoles }) {
const { user } = useAuth();
const location = useLocation();
if (!user) {
return <Navigate to="/login" state={{ from: location }} replace />;
}
if (!allowedRoles.includes(user.role)) {
return <Navigate to="/unauthorized" replace />;
}
return children;
}
// Usage
<Route
path="/admin"
element={
<RequireRole allowedRoles={['admin']}>
<AdminDashboard />
</RequireRole>
}
/>
<Route
path="/moderator"
element={
<RequireRole allowedRoles={['admin', 'moderator']}>
<ModeratorPanel />
</RequireRole>
}
/>
Permission-Based Protection
function RequirePermission({ children, permissions }) {
const { user } = useAuth();
if (!user) {
return <Navigate to="/login" replace />;
}
const hasPermission = permissions.every(permission =>
user.permissions?.includes(permission)
);
if (!hasPermission) {
return (
<div>
<h1>Access Denied</h1>
<p>You don't have permission to view this page.</p>
<Link to="/">Go to Home</Link>
</div>
);
}
return children;
}
// Usage
<Route
path="/users/manage"
element={
<RequirePermission permissions={['users.read', 'users.write']}>
<UserManagement />
</RequirePermission>
}
/>
Advanced Protection Patterns
Outlet-Based Protection
function ProtectedLayout() {
const { user, loading } = useAuth();
if (loading) {
return <LoadingSpinner />;
}
if (!user) {
return <Navigate to="/login" replace />;
}
return (
<div className="protected-layout">
<Header user={user} />
<Sidebar />
<main>
<Outlet />
</main>
</div>
);
}
// Routes configuration
<Routes>
<Route path="/login" element={<Login />} />
<Route element={<ProtectedLayout />}>
<Route path="/dashboard" element={<Dashboard />} />
<Route path="/profile" element={<Profile />} />
<Route path="/settings" element={<Settings />} />
</Route>
</Routes>
Multi-Level Protection
function App() {
return (
<Routes>
{/* Public routes */}
<Route path="/" element={<Home />} />
<Route path="/login" element={<Login />} />
<Route path="/register" element={<Register />} />
{/* Protected routes - requires authentication */}
<Route element={<RequireAuth />}>
<Route path="/dashboard" element={<Dashboard />} />
<Route path="/profile" element={<Profile />} />
{/* Admin routes - requires admin role */}
<Route element={<RequireRole allowedRoles={['admin']} />}>
<Route path="/admin" element={<AdminDashboard />} />
<Route path="/admin/users" element={<UserManagement />} />
<Route path="/admin/settings" element={<AdminSettings />} />
</Route>
{/* Moderator routes */}
<Route element={<RequireRole allowedRoles={['admin', 'moderator']} />}>
<Route path="/moderate" element={<ModerationPanel />} />
<Route path="/moderate/reports" element={<Reports />} />
</Route>
</Route>
{/* Catch all */}
<Route path="*" element={<NotFound />} />
</Routes>
);
}
Redirect After Login
Preserving Intended Destination
function Login() {
const navigate = useNavigate();
const location = useLocation();
const { login } = useAuth();
const [credentials, setCredentials] = useState({ email: '', password: '' });
const [error, setError] = useState('');
const from = location.state?.from?.pathname || '/dashboard';
const handleSubmit = async (e) => {
e.preventDefault();
setError('');
const result = await login(credentials);
if (result.success) {
// Redirect to the page they tried to visit
navigate(from, { replace: true });
} else {
setError(result.error);
}
};
return (
<form onSubmit={handleSubmit}>
<h1>Login</h1>
{location.state?.from && (
<p>You must log in to view {from}</p>
)}
{error && <div className="error">{error}</div>}
<input
type="email"
value={credentials.email}
onChange={(e) => setCredentials({...credentials, email: e.target.value})}
placeholder="Email"
required
/>
<input
type="password"
value={credentials.password}
onChange={(e) => setCredentials({...credentials, password: e.target.value})}
placeholder="Password"
required
/>
<button type="submit">Login</button>
</form>
);
}
Loading States and Suspense
Async Authentication Check
function ProtectedRoute({ children }) {
const { user, loading } = useAuth();
const location = useLocation();
if (loading) {
return (
<div className="loading-container">
<Spinner />
<p>Checking authentication...</p>
</div>
);
}
if (!user) {
return <Navigate to="/login" state={{ from: location }} replace />;
}
return children;
}
With React Suspense
const Dashboard = lazy(() => import('./pages/Dashboard'));
function ProtectedRoute({ children }) {
const { user } = useAuth();
if (!user) {
return <Navigate to="/login" replace />;
}
return (
<Suspense fallback={<LoadingSpinner />}>
{children}
</Suspense>
);
}
Session Management
Session Timeout
function SessionProvider({ children }) {
const [user, setUser] = useState(null);
const [lastActivity, setLastActivity] = useState(Date.now());
const navigate = useNavigate();
const SESSION_TIMEOUT = 30 * 60 * 1000; // 30 minutes
// Reset activity timer on user interaction
useEffect(() => {
const updateActivity = () => setLastActivity(Date.now());
window.addEventListener('click', updateActivity);
window.addEventListener('keypress', updateActivity);
return () => {
window.removeEventListener('click', updateActivity);
window.removeEventListener('keypress', updateActivity);
};
}, []);
// Check for session timeout
useEffect(() => {
const interval = setInterval(() => {
if (user && Date.now() - lastActivity > SESSION_TIMEOUT) {
// Session expired
logout();
navigate('/login', {
state: { message: 'Your session has expired. Please login again.' }
});
}
}, 60000); // Check every minute
return () => clearInterval(interval);
}, [user, lastActivity, navigate]);
const logout = () => {
setUser(null);
localStorage.removeItem('authToken');
};
return (
<AuthContext.Provider value={{ user, setUser, logout }}>
{children}
</AuthContext.Provider>
);
}
Route Guards
Custom Route Guard Hook
function useRouteGuard(guardFn, redirectTo = '/') {
const navigate = useNavigate();
const location = useLocation();
useEffect(() => {
const checkGuard = async () => {
const canAccess = await guardFn();
if (!canAccess) {
navigate(redirectTo, {
state: { from: location },
replace: true
});
}
};
checkGuard();
}, [guardFn, navigate, redirectTo, location]);
}
// Usage
function PremiumContent() {
useRouteGuard(
async () => {
const user = await getCurrentUser();
return user?.subscription === 'premium';
},
'/upgrade'
);
return <div>Premium content here</div>;
}
Multiple Guards
function GuardedRoute({ children, guards = [] }) {
const [canAccess, setCanAccess] = useState(null);
const [guardError, setGuardError] = useState(null);
useEffect(() => {
const checkGuards = async () => {
try {
for (const guard of guards) {
const result = await guard();
if (!result.success) {
setGuardError(result);
setCanAccess(false);
return;
}
}
setCanAccess(true);
} catch (error) {
setGuardError({ message: 'An error occurred' });
setCanAccess(false);
}
};
checkGuards();
}, [guards]);
if (canAccess === null) {
return <LoadingSpinner />;
}
if (!canAccess) {
return <Navigate to={guardError.redirectTo || '/login'} replace />;
}
return children;
}
// Guards
const isAuthenticated = async () => {
const token = localStorage.getItem('authToken');
return {
success: !!token,
redirectTo: '/login'
};
};
const hasSubscription = async () => {
const user = await getCurrentUser();
return {
success: user?.hasActiveSubscription,
redirectTo: '/subscribe'
};
};
const isEmailVerified = async () => {
const user = await getCurrentUser();
return {
success: user?.emailVerified,
redirectTo: '/verify-email'
};
};
// Usage
<Route
path="/premium-features"
element={
<GuardedRoute guards={[isAuthenticated, hasSubscription, isEmailVerified]}>
<PremiumFeatures />
</GuardedRoute>
}
/>
Complete Example
Full Authentication System
// AuthContext.js
const AuthContext = createContext(null);
export function AuthProvider({ children }) {
const [state, setState] = useState({
user: null,
loading: true,
error: null
});
useEffect(() => {
checkAuth();
}, []);
const checkAuth = async () => {
try {
const token = localStorage.getItem('authToken');
if (token) {
const user = await fetchUser(token);
setState({ user, loading: false, error: null });
} else {
setState({ user: null, loading: false, error: null });
}
} catch (error) {
setState({ user: null, loading: false, error: error.message });
localStorage.removeItem('authToken');
}
};
const value = {
...state,
login: async (credentials) => {
try {
const { user, token } = await loginAPI(credentials);
localStorage.setItem('authToken', token);
setState({ user, loading: false, error: null });
return { success: true };
} catch (error) {
return { success: false, error: error.message };
}
},
logout: () => {
localStorage.removeItem('authToken');
setState({ user: null, loading: false, error: null });
},
checkAuth
};
return (
<AuthContext.Provider value={value}>
{children}
</AuthContext.Provider>
);
}
// ProtectedRoute.js
export function ProtectedRoute({
children,
requireAuth = true,
requireRoles = [],
requirePermissions = [],
fallback = '/login'
}) {
const { user, loading } = useAuth();
const location = useLocation();
if (loading) {
return <LoadingSpinner />;
}
if (requireAuth && !user) {
return <Navigate to={fallback} state={{ from: location }} replace />;
}
if (requireRoles.length > 0 && !requireRoles.includes(user?.role)) {
return <Navigate to="/unauthorized" replace />;
}
if (requirePermissions.length > 0) {
const hasPermissions = requirePermissions.every(
permission => user?.permissions?.includes(permission)
);
if (!hasPermissions) {
return <Navigate to="/forbidden" replace />;
}
}
return children;
}
// App.js
function App() {
return (
<AuthProvider>
<BrowserRouter>
<Routes>
{/* Public routes */}
<Route path="/login" element={<Login />} />
<Route path="/register" element={<Register />} />
<Route path="/forgot-password" element={<ForgotPassword />} />
{/* Protected routes */}
<Route
path="/dashboard"
element={
<ProtectedRoute>
<Dashboard />
</ProtectedRoute>
}
/>
{/* Admin routes */}
<Route
path="/admin/*"
element={
<ProtectedRoute requireRoles={['admin']}>
<AdminLayout />
</ProtectedRoute>
}
>
<Route index element={<AdminDashboard />} />
<Route path="users" element={<UserManagement />} />
<Route path="settings" element={<AdminSettings />} />
</Route>
{/* Error pages */}
<Route path="/unauthorized" element={<Unauthorized />} />
<Route path="/forbidden" element={<Forbidden />} />
<Route path="*" element={<NotFound />} />
</Routes>
</BrowserRouter>
</AuthProvider>
);
}
Best Practices
- Always validate on the server - Client-side protection is not enough
- Handle loading states - Show spinner while checking auth
- Preserve navigation intent - Redirect to intended page after login
- Clear error messages - Explain why access was denied
- Implement session timeout - For security
- Use HTTPS - Always use secure connections
- Store tokens securely - Consider httpOnly cookies
- Refresh tokens - Implement token refresh mechanism
Protected routes are crucial for application security. Implement them carefully to balance security with user experience!