Protected Routes in React: Authentication and Redirects

Protect routes in React by wrapping private pages in a guard component. Redirect unauthenticated users to login and send them back after sign in.

5 min read

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.

App.jsxApp.jsx
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.

App.jsxApp.jsx
<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.

App.jsxApp.jsx
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

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.
RunePowered by Rune AI

Frequently Asked Questions

Should I hide the link or guard the route?

Guard the route. Hiding a link only hides the entry point, so a user can still open the protected URL directly in the address bar.

Why use Navigate instead of useNavigate?

Navigate is the declarative form. It renders nothing and redirects during render, which fits a guard component in the route tree.

How do I send users back after login?

Pass the attempted location through Navigate state, then read it after sign in and navigate back to that path.

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.