Skip to content

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>

The official React Router documentation recommends using a wrapper component that conditionally renders children or redirects to a login page.

Basic Implementation

jsx
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

jsx
<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:

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

const PrivateRoute = () => {
  const isAuthenticated = checkAuth();
  
  return isAuthenticated ? <Outlet /> : <Navigate to="/login" />;
};
jsx
<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:

jsx
// 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:

jsx
// 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

  1. Preserve navigation state: When redirecting to login, preserve the intended destination:
jsx
<Navigate to="/login" state={{ from: location }} replace />
  1. Handle authentication checks asynchronously if needed:
jsx
const PrivateRoute = ({ children }) => {
  const [isAuthenticated, setIsAuthenticated] = useState(null);
  
  useEffect(() => {
    checkAuthAsync().then(setIsAuthenticated);
  }, []);

  if (isAuthenticated === null) return <LoadingSpinner />;
  
  return isAuthenticated ? children : <Navigate to="/login" />;
};
  1. 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:

jsx
// 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.