Handling private routes incorrectly

Loading

Proper Private Route Implementation in React Router

A common React Router mistake is incorrectly handling private/protected routes, leading to security vulnerabilities or poor user experience. Here’s how to implement them properly in React Router v6.

The Problem (Common Anti-Patterns)

// ❌ Wrong - Insecure redirect (v5 pattern in v6)
<Route path="/dashboard" render={() => (
  isAuthenticated ? <Dashboard /> : <Redirect to="/login" />
)} />

// ❌ Wrong - No route protection
<Route path="/profile" element={<Profile />} />

// ❌ Wrong - Using components incorrectly
<PrivateRoute path="/admin" element={<AdminPanel />} />

Correct Solutions

1. Protected Route Wrapper (Recommended)

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

function PrivateRoute() {
  const { currentUser } = useAuth();

  return currentUser ? <Outlet /> : <Navigate to="/login" />;
}

// Usage
<Route element={<PrivateRoute />}>
  <Route path="/dashboard" element={<Dashboard />} />
  <Route path="/profile" element={<Profile />} />
</Route>

2. With Redirect Back

function PrivateRoute() {
  const location = useLocation();
  const { isAuthenticated } = useAuth();

  if (!isAuthenticated) {
    return <Navigate to="/login" state={{ from: location }} replace />;
  }

  return <Outlet />;
}

3. Role-Based Protection

function AdminRoute({ 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 <Outlet />;
}

// Usage
<Route element={<AdminRoute allowedRoles={['admin', 'superadmin']} />}>
  <Route path="/admin" element={<AdminPanel />} />
</Route>

Key Principles

  1. Authentication Check: Verify user is logged in
  2. Authorization Check: Verify user has required permissions
  3. State Preservation: Remember where user came from
  4. Proper Redirects: Use replace to maintain history
  5. Layout Preservation: Keep consistent layouts for protected routes

Common Mistakes to Avoid

  1. Checking auth in components:
   function Dashboard() {
     // ❌ Too late - already rendered
     if (!isAuthenticated) return <Navigate to="/login" />;
   }
  1. Forgetting the replace prop:
   <Navigate to="/login" /> // ❌ Adds to history stack
  1. Not passing route state:
   // ❌ Can't redirect back after login
   <Navigate to="/login" />
  1. Mixing v5 and v6 patterns:
   // ❌ v5 syntax doesn't work in v6
   <PrivateRoute path="/admin" component={Admin} />

Best Practices

  1. Centralize auth logic in route protection
  2. Use layout routes for consistent UI
  3. TypeScript users – Add type safety:
   interface AuthState {
     from?: string;
   }

   <Navigate to="/login" state={{ from: location.pathname } as AuthState} />
  1. Handle loading states:
   function PrivateRoute() {
     const { user, isLoading } = useAuth();

     if (isLoading) return <LoadingScreen />;
     if (!user) return <Navigate to="/login" replace />;

     return <Outlet />;
   }

Advanced Patterns

1. Data Loading Protection

function ProtectedLoader({ loader }) {
  const { user } = useAuth();

  if (!user) {
    throw new Response('Unauthorized', { status: 401 });
  }

  return loader();
}

// In router config
{
  path: '/dashboard',
  element: <DashboardLayout />,
  loader: () => ProtectedLoader({
    loader: () => fetchDashboardData()
  }),
  children: [/*...*/]
}

2. Route-Level Metadata

function PrivateRoute({ roles }) {
  const { user } = useAuth();

  // ...auth checks

  return (
    <AuthContext.Provider value={{ user, roles }}>
      <Outlet />
    </AuthContext.Provider>
  );
}

3. Expired Session Handling

function PrivateRoute() {
  const { user, isExpired } = useAuth();
  const location = useLocation();

  if (isExpired) {
    return <Navigate to="/session-expired" state={{ from: location }} replace />;
  }

  if (!user) {
    return <Navigate to="/login" state={{ from: location }} replace />;
  }

  return <Outlet />;
}

Remember: Proper route protection requires:

  1. Early checks – Before rendering protected content
  2. Secure patterns – Don’t rely on client-side checks alone
  3. Good UX – Preserve navigation state and context
  4. Clear architecture – Keep auth logic maintainable

Always combine client-side protection with server-side validation for truly secure applications.

Leave a Reply

Your email address will not be published. Required fields are marked *