Private Routes in React Router v6
React Router v6 introduced several breaking changes that affect how we implement protected routes. This article explains the correct patterns for creating private routes in your React applications.
The Problem
In React Router v6, all children of the <Routes>
component must be <Route>
or <React.Fragment>
elements. This means the previous v5 pattern of creating custom route components like <PrivateRoute>
no longer works.
The common error you'll encounter is:
Error: [PrivateRoute] is not a <Route> component. All component children of <Routes> must be a <Route> or <React.Fragment>
Recommended Solution: Wrapper Component Pattern
The official React Router documentation recommends using a wrapper component that conditionally renders children or redirects to a login page.
Basic Implementation
import { Navigate, useLocation } from 'react-router-dom';
const PrivateRoute = ({ children }) => {
const isAuthenticated = checkAuth(); // Your authentication logic
const location = useLocation();
return isAuthenticated ? (
children
) : (
<Navigate to="/login" state={{ from: location }} replace />
);
};
Usage in Routes
<Routes>
<Route
path="/dashboard"
element={
<PrivateRoute>
<Dashboard />
</PrivateRoute>
}
/>
<Route path="/login" element={<Login />} />
<Route path="/" element={<Home />} />
</Routes>
Advanced Pattern: Using Outlet
For more complex routing scenarios, you can use the <Outlet>
component:
import { Navigate, Outlet } from 'react-router-dom';
const PrivateRoute = () => {
const isAuthenticated = checkAuth();
return isAuthenticated ? <Outlet /> : <Navigate to="/login" />;
};
<Routes>
<Route element={<PrivateRoute />}>
<Route path="/dashboard" element={<Dashboard />} />
<Route path="/profile" element={<Profile />} />
</Route>
<Route path="/login" element={<Login />} />
</Routes>
Alternative Approaches
Function-based Routes
Some developers prefer a function-based approach to avoid wrapper components:
// PrivateRoute.jsx
const PrivateRoute = ({ element, path }) => {
const isAuthenticated = checkAuth();
return (
<Route
path={path}
element={isAuthenticated ? element : <Navigate to="/login" />}
/>
);
};
// In your router configuration
<Routes>
{PrivateRoute({ element: <Dashboard />, path: '/dashboard' })}
<Route path="/login" element={<Login />} />
</Routes>
Route Configuration Object
For large applications, consider using a route configuration object:
// routes.js
export const publicRoutes = [
{ path: '/', element: <Home /> },
{ path: '/login', element: <Login /> },
];
export const privateRoutes = [
{ path: '/dashboard', element: <Dashboard /> },
{ path: '/profile', element: <Profile /> },
];
// App.jsx
<Routes>
{publicRoutes.map((route) => (
<Route key={route.path} {...route} />
))}
{privateRoutes.map((route) => (
<Route
key={route.path}
path={route.path}
element={
<PrivateRoute>
{route.element}
</PrivateRoute>
}
/>
))}
</Routes>
Best Practices
- Preserve navigation state: When redirecting to login, preserve the intended destination:
<Navigate to="/login" state={{ from: location }} replace />
- Handle authentication checks asynchronously if needed:
const PrivateRoute = ({ children }) => {
const [isAuthenticated, setIsAuthenticated] = useState(null);
useEffect(() => {
checkAuthAsync().then(setIsAuthenticated);
}, []);
if (isAuthenticated === null) return <LoadingSpinner />;
return isAuthenticated ? children : <Navigate to="/login" />;
};
- Use context for authentication: Consider using React Context to manage authentication state across your application.
WARNING
Avoid creating custom Route components that aren't actual <Route>
elements, as this will cause the error mentioned in the problem statement.
Migration from v5
If you're migrating from React Router v5, note that these patterns replace the previous approach where you could create custom route components:
// v5 pattern (no longer works in v6)
<PrivateRoute path="/dashboard" component={Dashboard} />
Conclusion
React Router v6 encourages a different approach to protected routes than v5. By using wrapper components or the Outlet component, you can create clean, maintainable authentication patterns that work with the v6 architecture.
The key insight is that in v6, you protect the content (the element
prop) rather than the route itself, which leads to more flexible and composable routing patterns.
For more information, consult the official React Router documentation and their authentication examples.