![]()
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
- Authentication Check: Verify user is logged in
- Authorization Check: Verify user has required permissions
- State Preservation: Remember where user came from
- Proper Redirects: Use
replaceto maintain history - Layout Preservation: Keep consistent layouts for protected routes
Common Mistakes to Avoid
- Checking auth in components:
function Dashboard() {
// ❌ Too late - already rendered
if (!isAuthenticated) return <Navigate to="/login" />;
}
- Forgetting the replace prop:
<Navigate to="/login" /> // ❌ Adds to history stack
- Not passing route state:
// ❌ Can't redirect back after login
<Navigate to="/login" />
- Mixing v5 and v6 patterns:
// ❌ v5 syntax doesn't work in v6
<PrivateRoute path="/admin" component={Admin} />
Best Practices
- Centralize auth logic in route protection
- Use layout routes for consistent UI
- TypeScript users – Add type safety:
interface AuthState {
from?: string;
}
<Navigate to="/login" state={{ from: location.pathname } as AuthState} />
- 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:
- Early checks – Before rendering protected content
- Secure patterns – Don’t rely on client-side checks alone
- Good UX – Preserve navigation state and context
- Clear architecture – Keep auth logic maintainable
Always combine client-side protection with server-side validation for truly secure applications.
