Protected routes in React are pages that only signed-in users can open. A guard component checks the user, renders the page through an Outlet when allowed, and redirects to the login page when not. This article uses React Router's declarative BrowserRouter mode, the simplest setup for a client-side app.
Build a guard component
The guard wraps the private routes and makes one decision: render the child, or redirect. It uses Navigate for the redirect and Outlet to show the protected page.
import { Navigate, Outlet, useLocation } from "react-router";
export default function RequireAuth() {
const location = useLocation();
const user = useAuth();
if (!user) {
return <Navigate to="/login" state={{ from: location }} replace />;
}
return <Outlet />;
}The useAuth hook comes from your authentication layer and returns the current user or null. When the user is missing, Navigate replaces the current history entry with /login and records the attempted location in state. Otherwise Outlet renders the matched child route.
React Router's docs generally point you to useNavigate for everyday navigation, but useNavigate is not safe to call during render, only from an event handler or an Effect. A guard component needs to redirect during render itself, so Navigate is still the right tool here: it renders nothing and performs the redirect as part of rendering the guard.
Storing the attempted location in state is what enables the redirect-back behavior in the next section.
Wrap the private routes
Put the guard on a parent route so every child is protected at once. Public routes like login sit outside the guard.
<Routes>
<Route path="/login" element={<Login />} />
<Route element={<RequireAuth />}>
<Route path="/dashboard" element={<Dashboard />} />
<Route path="/settings" element={<Settings />} />
</Route>
</Routes>The parent route has no path, so it adds the guard without changing the URL. Visiting /dashboard while signed out redirects to /login instead of rendering Dashboard.
The check runs when the route renders, not only when a link is clicked. A user can still type /dashboard into the address bar, and the guard still catches it.
Children of the guard inherit the same check, so you can nest settings and profile pages under one protected parent.
Return the user to the page they wanted
The guard stored the original location in state. Read it after login and send the user back.
import { useLocation, useNavigate } from "react-router";
export default function Login() {
const location = useLocation();
const navigate = useNavigate();
const from = location.state?.from?.pathname || "/dashboard";
function handleLogin() {
signIn();
navigate(from, { replace: true });
}
return <button onClick={handleLogin}>Log in</button>;
}After signIn, the app navigates to the path the user originally requested. The replace option stops the back button from returning to the login page. Programmatic navigation is covered in how to navigate programmatically with useNavigate.
Redirect inside loaders in data mode
The same idea exists in React Router's data mode. A loader can throw redirect("/login") before returning data, and the router handles it as a redirect. That pattern and the rest of the data mode APIs are covered in React Router loaders and actions explained.
The router stops the navigation before the page renders, so protected data never loads for a signed-out user.
Common mistakes
- Checking auth only in the link, so a direct URL still opens the page.
- Forgetting replace on the redirect, so the back button loops to login.
- Redirecting before the auth state has finished loading.
Rune AI
Key Insights
- Wrap private routes in a guard component that checks the user.
- Render Outlet for allowed users and Navigate to login otherwise.
- Save the attempted location and return to it after sign in.
- Use redirect from a loader in data mode.
Frequently Asked Questions
Should I hide the link or guard the route?
Why use Navigate instead of useNavigate?
How do I send users back after login?
Conclusion
Protect routes with a guard component that renders an Outlet for signed-in users and redirects everyone else to login. Store the attempted location and return to it after sign in. In data mode, throw redirect from a loader instead.
More in this topic
How to Build a Dropdown Menu in React
Build a React dropdown menu with the ARIA menu button pattern. Handle open and close, keyboard arrows, and clicks outside the menu.
How to Animate React Components with Motion
Animate React components with the Motion library. Set up motion, add enter, hover, and exit animations, and respect reduced motion.
Headless UI Components Explained: Logic Without Locked Styling
Understand headless UI components and how libraries like Radix give you unstyled, accessible behavior that you style yourself.