Navigation Guards
In this tutorial, we will learn how to implement navigation guards in React Router 7 using modern React 19 patterns. Navigation guards control who can access a route and when navigation is allowed.
What We Will Learn
- What navigation guards are
- When navigation guards are needed
- Protecting routes with loaders
- Redirecting unauthenticated users
- Guarding entire route sections
- Blocking navigation based on conditions
- Best practices for route protection
What Is a Navigation Guard
A navigation guard is logic that runs before a route renders.
Common use cases:
- Authentication checks
- Authorization and roles
- Preventing access to private pages
- Redirecting users to login
- Blocking navigation when data is unsaved
In React Router 7, guards are implemented at the routing level, not inside components.
Why Guards Belong in the Router
Putting guard logic in components causes problems:
- UI flashes before redirect
- Logic is duplicated
- Routes are harder to reason about
React Router 7 encourages data first routing, where checks happen before rendering.
Guarding Routes with Loaders
The primary way to create navigation guards is using loaders.
A loader runs before navigation completes.
Basic Folder Structure
/src
/app
app.tsx
root-layout.tsx
auth.ts
/pages
login.tsx
dashboard.tsx
main.tsx
Creating an Auth Utility
This simulates authentication state.
// src/app/auth.ts
export function isAuthenticated() {
return Boolean(localStorage.getItem("token"));
}Creating a Protected Loader
The loader checks authentication and redirects if needed.
// src/app/app.tsx
import { createBrowserRouter, redirect } from "react-router";
import RootLayout from "./root-layout";
import Dashboard from "../pages/dashboard";
import Login from "../pages/login";
import { isAuthenticated } from "./auth";
function requireAuth() {
if (!isAuthenticated()) {
throw redirect("/login");
}
return null;
}Throwing redirect immediately stops navigation.
Applying the Guard to a Route
// src/app/app.tsx
export const router = createBrowserRouter([
{
element: <RootLayout />,
children: [
{ path: "/login", element: <Login /> },
{
path: "/dashboard",
element: <Dashboard />,
loader: requireAuth,
},
],
},
]);Now /dashboard is protected.
Guarding an Entire Section with Nested Routes
Guards become more powerful with nested layouts.
// src/app/app.tsx
import DashboardLayout from "./dashboard-layout";
export const router = createBrowserRouter([
{
element: <RootLayout />,
children: [
{ path: "/login", element: <Login /> },
{
element: <DashboardLayout />,
loader: requireAuth,
children: [
{ path: "/dashboard", element: <Dashboard /> },
{ path: "/dashboard/settings", element: <Settings /> },
],
},
],
},
]);The loader runs for all child routes.
Creating the Login Page
// src/pages/login.tsx
"use client";
export default function Login() {
function handleLogin() {
localStorage.setItem("token", "demo");
window.location.href = "/dashboard";
}
return (
<>
<title>Login</title>
<h1>Login</h1>
<button onClick={handleLogin}>Sign In</button>
</>
);
}Once logged in, protected routes become accessible.
Preventing Access for Authenticated Users
You can also guard public routes like login pages.
function redirectIfAuthenticated() {
if (isAuthenticated()) {
throw redirect("/dashboard");
}
return null;
}{
path: "/login",
element: <Login />,
loader: redirectIfAuthenticated,
}Blocking Navigation Based on Conditions
Sometimes you want to block navigation, not redirect.
Example: unsaved form data.
React Router 7 provides navigation blocking through router APIs rather than effects inside components.
The recommended approach is:
- Keep form state in the route
- Prevent navigation via loader or action conditions
- Ask for confirmation before redirecting
This avoids unreliable component based guards.
Why Not Use Component Guards
Avoid patterns like:
- Redirecting inside components
- Using effects to navigate
- Conditional rendering for protection
These cause:
- UI flicker
- Inconsistent behavior
- Hard to maintain logic
Routing logic belongs in the router.
Best Practices for Navigation Guards
- Use loaders for access control
- Throw redirects instead of navigating manually
- Guard layouts, not individual pages when possible
- Keep auth logic centralized
- Treat routing as part of application state
Common Mistakes
- Checking auth inside components
- Using effects to redirect
- Protecting routes after render
- Duplicating guard logic
Conclusion
Navigation guards in React Router 7 are clean, predictable, and powerful when implemented with loaders. By moving access control into the router, you prevent UI flashes, reduce duplication, and keep your application easy to reason about.
If a user should never see a page, the router should stop them before it renders.