How to Secure Outlet Component in React Router

Securing the Outlet component in React Router is a fundamental pattern for protecting private routes from unauthorized access. This article provides a straightforward guide on how to create a reusable ProtectedRoute component that conditionalizes the rendering of the <Outlet /> based on user authentication, ensuring that restricted layout views are only accessible to logged-in users.

Understanding the Secure Outlet Pattern

In React Router (v6+), the <Outlet /> component is used within a parent route element to render its child route elements. To secure these child routes, you wrap them in a protective parent route.

Instead of checking authentication on every individual page, you check it once in a custom layout component. If the user is authenticated, the component renders the <Outlet /> (allowing child components to load). If the user is not authenticated, they are redirected to a public page, such as a login screen.

Step-by-Step Implementation

1. Create the Protected Route Component

First, create a component called ProtectedRoute. This component will check your application’s authentication state (e.g., from a context, Redux, or local storage).

import { Navigate, Outlet } from 'react-router-dom';

const ProtectedRoute = ({ isAuthenticated }) => {
  // If the user is not authenticated, redirect to the login page
  if (!isAuthenticated) {
    return <Navigate to="/login" replace />;
  }

  // If authenticated, render the child routes via <Outlet />
  return <Outlet />;
};

export default ProtectedRoute;

2. Configure Your Routes

Next, integrate the ProtectedRoute into your router configuration. Wrap the routes that require authentication inside the ProtectedRoute route.

import { BrowserRouter, Routes, Route } from 'react-router-dom';
import ProtectedRoute from './ProtectedRoute';
import Dashboard from './pages/Dashboard';
import Profile from './pages/Profile';
import Login from './pages/Login';
import Home from './pages/Home';

function App() {
  // Replace this with your actual authentication state logic
  const { isAuthenticated } = useAuth(); 

  return (
    <BrowserRouter>
      <Routes>
        {/* Public Routes */}
        <Route path="/" element={<Home />} />
        <Route path="/login" element={<Login />} />

        {/* Protected Routes */}
        <Route element={<ProtectedRoute isAuthenticated={isAuthenticated} />}>
          <Route path="/dashboard" element={<Dashboard />} />
          <Route path="/profile" element={<Profile />} />
        </Route>
      </Routes>
    </BrowserRouter>
  );
}

How It Works

Adding Role-Based Authorization

You can extend this pattern to support role-based access control (RBAC) by passing allowed roles to your protected route wrapper.

const ProtectedRoute = ({ isAuthenticated, userRole, allowedRoles }) => {
  if (!isAuthenticated) {
    return <Navigate to="/login" replace />;
  }

  if (allowedRoles && !allowedRoles.includes(userRole)) {
    return <Navigate to="/unauthorized" replace />;
  }

  return <Outlet />;
};

You can then declare specific roles for your nested routes:

<Route 
  element={
    <ProtectedRoute 
      isAuthenticated={isAuthenticated} 
      userRole={user.role} 
      allowedRoles={['admin']} 
    />
  }
>
  <Route path="/admin-settings" element={<AdminSettings />} />
</Route>