In modern web applications, certain routes or pages should only be accessible to authenticated users. React Router, combined with the authentication context we built in the previous lesson, provides a powerful foundation for creating private routes and implementing route guards.
Private routes are components that restrict access to specific routes based on authentication status. If a user is not authenticated, they are redirected to a login page or another appropriate route instead of accessing the protected content.
The basic flow of private routes is:
There are multiple approaches to implementing private routes with React Router, depending on the version you're using. Let's explore the most common patterns, starting with the latest approach using React Router v6:
React Router v6 has simplified the way we can implement private routes. Instead of creating a dedicated PrivateRoute
component, we can leverage the new routing system to protect routes with greater flexibility:
1import { BrowserRouter, Routes, Route, Navigate, Outlet } from "react-router-dom"; 2import { useAuth } from "./context/AuthContext"; 3import Dashboard from "./pages/Dashboard"; 4import Profile from "./pages/Profile"; 5import Login from "./pages/Login"; 6import Home from "./pages/Home"; 7 8// Authentication guard component 9function RequireAuth({ children }) { 10 const { isAuthenticated, loading } = useAuth(); 11 12 // Show loading state or spinner while authentication is being checked 13 if (loading) { 14 return <div>Loading...</div>; 15 } 16 17 // Redirect to login if not authenticated 18 if (!isAuthenticated()) { 19 return <Navigate to="/login" />; 20 } 21 22 // Render children if authenticated 23 return children; 24} 25 26function App() { 27 return ( 28 <BrowserRouter> 29 <Routes> 30 <Route path="/" element={<Home />} /> 31 <Route path="/login" element={<Login />} /> 32 33 {/* Protected routes */} 34 <Route path="/dashboard" element={ 35 <RequireAuth> 36 <Dashboard /> 37 </RequireAuth> 38 } /> 39 40 <Route path="/profile" element={ 41 <RequireAuth> 42 <Profile /> 43 </RequireAuth> 44 } /> 45 46 {/* Protected route group using Outlet */} 47 <Route path="/admin" element={ 48 <RequireAuth> 49 <Outlet /> 50 </RequireAuth> 51 }> 52 <Route index element={<AdminDashboard />} /> 53 <Route path="users" element={<AdminUsers />} /> 54 <Route path="settings" element={<AdminSettings />} /> 55 </Route> 56 57 {/* Fallback route */} 58 <Route path="*" element={<NotFound />} /> 59 </Routes> 60 </BrowserRouter> 61 ); 62}
The RequireAuth
component acts as a wrapper for routes that need protection. It checks if the user is authenticated and either renders the protected component or redirects to the login page.
React Router v6 also introduces Outlet
which enables you to protect entire route groups with a single authentication check, as shown in the admin routes example.
If you're using React Router v5, the approach is slightly different. You'll create a custom PrivateRoute
component:
1import React from "react"; 2import { Route, Redirect } from "react-router-dom"; 3import { useAuth } from "./context/AuthContext"; 4 5// Private route component for React Router v5 6function PrivateRoute({ component: Component, ...rest }) { 7 const { isAuthenticated, loading } = useAuth(); 8 9 return ( 10 <Route 11 {...rest} 12 render={props => { 13 if (loading) { 14 return <div>Loading...</div>; 15 } 16 17 return isAuthenticated() ? ( 18 <Component {...props} /> 19 ) : ( 20 <Redirect 21 to={{ 22 pathname: "/login", 23 state: { from: props.location } 24 }} 25 /> 26 ); 27 }} 28 /> 29 ); 30}
And use it in your route setup:
1import { BrowserRouter, Switch, Route } from "react-router-dom"; 2import PrivateRoute from "./components/PrivateRoute"; 3import Dashboard from "./pages/Dashboard"; 4import Profile from "./pages/Profile"; 5import Login from "./pages/Login"; 6import Home from "./pages/Home"; 7 8function App() { 9 return ( 10 <BrowserRouter> 11 <Switch> 12 <Route exact path="/" component={Home} /> 13 <Route path="/login" component={Login} /> 14 15 <PrivateRoute path="/dashboard" component={Dashboard} /> 16 <PrivateRoute path="/profile" component={Profile} /> 17 18 <Route path="*" component={NotFound} /> 19 </Switch> 20 </BrowserRouter> 21 ); 22}
A common UX pattern is to redirect users back to the page they were trying to access after logging in. Let's implement this feature using React Router:
1import React, { useState } from "react"; 2import { useNavigate, useLocation } from "react-router-dom"; 3import { useAuth } from "../context/AuthContext"; 4 5function Login() { 6 const [email, setEmail] = useState(""); 7 const [password, setPassword] = useState(""); 8 const [error, setError] = useState(""); 9 10 const { login } = useAuth(); 11 const navigate = useNavigate(); 12 const location = useLocation(); 13 14 // Get the redirect path from location state or default to dashboard 15 const from = location.state?.from?.pathname || "/dashboard"; 16 17 const handleSubmit = async (e) => { 18 e.preventDefault(); 19 setError(""); 20 21 try { 22 await login(email, password); 23 // Redirect to the page they were trying to access 24 navigate(from, { replace: true }); 25 } catch (error) { 26 setError("Failed to sign in"); 27 } 28 }; 29 30 // Rest of component... 31}
When using React Router v5, the implementation is similar but uses different hooks:
1// React Router v5 version 2import { useHistory, useLocation } from "react-router-dom"; 3 4function Login() { 5 // ... 6 const history = useHistory(); 7 const location = useLocation(); 8 const { from } = location.state || { from: { pathname: "/dashboard" } }; 9 10 const handleSubmit = async (e) => { 11 // ... 12 try { 13 await login(email, password); 14 history.replace(from); 15 } catch (error) { 16 // ... 17 } 18 }; 19}
For more complex applications, you might need to restrict routes based on user roles. Let's extend our route protection to include role checks:
1function RequireRole({ children, role }) { 2 const { isAuthenticated, hasRole, loading } = useAuth(); 3 4 if (loading) { 5 return <div>Loading...</div>; 6 } 7 8 if (!isAuthenticated()) { 9 return <Navigate to="/login" />; 10 } 11 12 if (!hasRole(role)) { 13 return <Navigate to="/unauthorized" />; 14 } 15 16 return children; 17} 18 19// Usage in routes 20<Route path="/admin/dashboard" element={ 21 <RequireRole role="admin"> 22 <AdminDashboard /> 23 </RequireRole> 24} />
For applications with distinct layouts for authenticated and unauthenticated sections, you can combine private routes with layout components:
1function AuthenticatedLayout({ children }) { 2 return ( 3 <div className="authenticated-layout"> 4 <Sidebar /> 5 <div className="content-area"> 6 <TopNav /> 7 <main>{children}</main> 8 </div> 9 </div> 10 ); 11} 12 13// In your routes config 14<Route path="/dashboard" element={ 15 <RequireAuth> 16 <AuthenticatedLayout> 17 <Dashboard /> 18 </AuthenticatedLayout> 19 </RequireAuth> 20} />
For more complex applications with nested route structures, React Router v6's composition model works well:
1<Routes> 2 {/* Public routes */} 3 <Route path="/" element={<PublicLayout />}> 4 <Route index element={<Home />} /> 5 <Route path="about" element={<About />} /> 6 <Route path="login" element={<Login />} /> 7 </Route> 8 9 {/* Protected routes */} 10 <Route path="/app" element={ 11 <RequireAuth> 12 <AuthenticatedLayout /> 13 </RequireAuth> 14 }> 15 <Route index element={<Dashboard />} /> 16 <Route path="profile" element={<Profile />} /> 17 <Route path="settings" element={<Settings />} /> 18 19 {/* Nested protected routes with additional role requirements */} 20 <Route path="admin" element={ 21 <RequireRole role="admin"> 22 <AdminLayout /> 23 </RequireRole> 24 }> 25 <Route index element={<AdminDashboard />} /> 26 <Route path="users" element={<AdminUsers />} /> 27 </Route> 28 </Route> 29</Routes>
For better performance in larger applications, you can combine route protection with lazy loading:
1import React, { lazy, Suspense } from "react"; 2 3// Lazy-loaded components 4const Dashboard = lazy(() => import("./pages/Dashboard")); 5const Settings = lazy(() => import("./pages/Settings")); 6const Profile = lazy(() => import("./pages/Profile")); 7 8function App() { 9 return ( 10 <BrowserRouter> 11 <Routes> 12 <Route path="/" element={<Home />} /> 13 <Route path="/login" element={<Login />} /> 14 15 <Route path="/app" element={ 16 <RequireAuth> 17 <Suspense fallback={<div>Loading...</div>}> 18 <Outlet /> 19 </Suspense> 20 </RequireAuth> 21 }> 22 <Route index element={<Dashboard />} /> 23 <Route path="profile" element={<Profile />} /> 24 <Route path="settings" element={<Settings />} /> 25 </Route> 26 </Routes> 27 </BrowserRouter> 28 ); 29}
Private routes are essential for protecting sensitive content in React applications. By combining React Router with our authentication context, we've created a robust system for route protection that can scale from simple applications to complex, role-based access control systems.
The patterns shown in this lesson provide a solid foundation for implementing authentication guards in your React applications, ensuring that only authorized users can access protected content while providing a smooth user experience.