Difficulty: Intermediate
Route protection is a fundamental pattern in React applications that restricts access to certain routes based on authentication status, user roles, or other conditions. The most common implementation is a ProtectedRoute (or RequireAuth) wrapper component that checks if the user is authenticated and either renders the child content or redirects to the login page.
The standard ProtectedRoute pattern is a component that wraps protected content. It checks an authentication condition (typically from a context or global state store). If the user is authenticated, it renders its children (or an Outlet for nested routes). If not, it renders a Navigate component that redirects to the login page. The redirect typically includes the current location in the state so the login page can redirect back after successful authentication.
For role-based access control, the ProtectedRoute can accept additional props like requiredRole or permissions. The component then checks not just if the user is logged in, but if they have the appropriate role or permissions. Unauthorized but authenticated users might be redirected to an 'access denied' page rather than the login page.
The login redirect flow works as follows: (1) User visits /dashboard without being logged in. (2) ProtectedRoute detects no auth and redirects to /login with state: { from: '/dashboard' }. (3) User logs in successfully. (4) Login handler reads location.state.from and navigates to '/dashboard' with replace: true. The replace option ensures the login page doesn't stay in the history stack.
ProtectedRoute can be used in two ways: wrapping individual routes or wrapping groups of routes using layout routes. The layout route approach is cleaner for protecting many routes: define a parent route with ProtectedRoute as the element, and nest all protected routes inside it. This avoids wrapping every single route individually.
// Simulating the ProtectedRoute pattern
function ProtectedRoute({ isAuthenticated, children, redirectTo }) {
if (!isAuthenticated) {
console.log('ProtectedRoute: Not authenticated');
console.log('Redirecting to: ' + redirectTo);
return null; // In real code: return <Navigate to={redirectTo} replace />
}
console.log('ProtectedRoute: Access granted');
return children;
}
// Unauthenticated user
console.log('--- Guest visits /dashboard ---');
ProtectedRoute({
isAuthenticated: false,
children: 'Dashboard Content',
redirectTo: '/login'
});
// Authenticated user
console.log('');
console.log('--- Logged in user visits /dashboard ---');
const content = ProtectedRoute({
isAuthenticated: true,
children: 'Dashboard Content',
redirectTo: '/login'
});
console.log('Rendered:', content);
ProtectedRoute checks authentication and either renders children or redirects. This single component can protect any route by wrapping its content.
function RoleGuard({ user, requiredRole, children }) {
if (!user) {
console.log('RoleGuard: No user -> redirect to /login');
return null;
}
if (user.role !== requiredRole) {
console.log('RoleGuard: User role "' + user.role + '" != required "' + requiredRole + '"');
console.log('RoleGuard: Redirect to /unauthorized');
return null;
}
console.log('RoleGuard: Access granted for ' + user.name + ' (' + user.role + ')');
return children;
}
// No user
RoleGuard({ user: null, requiredRole: 'admin', children: 'Admin Panel' });
console.log('');
// Wrong role
RoleGuard({ user: { name: 'Bob', role: 'viewer' }, requiredRole: 'admin', children: 'Admin Panel' });
console.log('');
// Correct role
const result = RoleGuard({ user: { name: 'Alice', role: 'admin' }, requiredRole: 'admin', children: 'Admin Panel' });
console.log('Content:', result);
Role-based guards extend authentication checks with authorization. Unauthenticated users go to login, authenticated users without the right role go to an unauthorized page, and authorized users see the content.
function simulateAuthFlow() {
let isLoggedIn = false;
let savedLocation = null;
function visitProtectedPage(page) {
console.log('Visiting: ' + page);
if (!isLoggedIn) {
savedLocation = page;
console.log('Not logged in -> redirect to /login');
console.log('Saved return location: ' + page);
} else {
console.log('Authenticated -> showing ' + page);
}
}
function login(email) {
console.log('Logging in as: ' + email);
isLoggedIn = true;
if (savedLocation) {
console.log('Redirect back to: ' + savedLocation + ' (replace: true)');
const destination = savedLocation;
savedLocation = null;
return destination;
}
console.log('No saved location -> redirect to /home');
return '/home';
}
// Step 1: Visit protected page
visitProtectedPage('/dashboard/reports');
console.log('');
// Step 2: Login
const redirect = login('alice@test.com');
console.log('');
// Step 3: Access granted
visitProtectedPage(redirect);
}
simulateAuthFlow();
The full auth flow: (1) user tries to access protected page, (2) gets redirected to login with the original location saved, (3) logs in, (4) gets redirected back to the originally requested page.
protected routes, auth guards, redirect on login, route wrappers, authentication flow