Protected Routes in React
Protected routes (or route guards) are a crucial pattern in web applications to restrict access to certain pages based on the user's authentication or authorization status. If a user is not authenticated, they are typically redirected to a login page.
Core Concept
In a React application using React Router, a protected route is simply a wrapper component around your restricted content. This wrapper checks the user's authentication state.
- If authenticated: It renders the child components (the protected content).
- If not authenticated: It uses the
<Navigate>component to redirect the user to a public route, like/login.
Basic Implementation with React Router v6
Here is a standard approach to implementing protected routes:
1. The ProtectedRoute Component
import { Navigate, Outlet, useLocation } from 'react-router-dom';
import { useAuth } from './AuthProvider'; // Assume this provides { user, isLoading }
export const ProtectedRoute = ({ children }) => {
const { user, isLoading } = useAuth();
const location = useLocation();
if (isLoading) {
return <div>Loading...</div>; // Prevent redirect while checking auth state
}
if (!user) {
// Redirect to login, but save the current location they were trying to go to
return <Navigate to="/login" state={{ from: location }} replace />;
}
// If children is provided, render it. Otherwise, render Outlet (for nested routes)
return children ? children : <Outlet />;
};
2. Using the ProtectedRoute in your Router
You can use this component either as a wrapper for individual routes or as a layout route for multiple nested routes.
import { Routes, Route } from 'react-router-dom';
import { ProtectedRoute } from './ProtectedRoute';
import Dashboard from './Dashboard';
import Settings from './Settings';
import Login from './Login';
function App() {
return (
<Routes>
{/* Public Routes */}
<Route path="/login" element={<Login />} />
{/* Protected Routes using Layout Pattern (Outlet) */}
<Route element={<ProtectedRoute />}>
<Route path="/dashboard" element={<Dashboard />} />
<Route path="/settings" element={<Settings />} />
</Route>
</Routes>
);
}
Advanced Patterns
Role-Based Access Control (RBAC)
Sometimes authentication isn't enough; you need authorization based on roles (e.g., Admin vs User).
export const RoleProtectedRoute = ({ allowedRoles, children }) => {
const { user, isLoading } = useAuth();
if (isLoading) return <div>Loading...</div>;
if (!user) return <Navigate to="/login" replace />;
if (!allowedRoles.includes(user.role)) {
return <Navigate to="/unauthorized" replace />;
}
return children ? children : <Outlet />;
};
Redirecting After Login
Notice how we passed state={{ from: location }} in the Navigate component? This is useful for redirecting users back to where they wanted to go after they successfully log in.
// Inside Login component
import { useNavigate, useLocation } from 'react-router-dom';
function Login() {
const navigate = useNavigate();
const location = useLocation();
const { login } = useAuth();
// The path the user was trying to access before being redirected
const from = location.state?.from?.pathname || "/dashboard";
const handleLogin = async () => {
await login('username', 'password');
// Replace history so the user can't use the back button to go to login page
navigate(from, { replace: true });
};
return <button onClick={handleLogin}>Log In</button>;
}
Summary
- Use wrapper components to conditionally render routes or redirect using
<Navigate>. - Handle loading states to prevent premature redirects.
- Leverage
location.stateto improve UX by redirecting users back to their intended destination post-login.