# React Router User Guide

React Router is the standard routing library for React applications. It supports three distinct usage modes — declarative routing, data routers, and full-stack framework mode — giving you the flexibility to use it as a simple client-side router or a complete application framework with server-side rendering, data loading, and mutations.

This guide covers everything you need to install, configure, and use React Router effectively in any of its modes.

---

## 1. Quick Start

### 1.1 Create a New Project (Framework Mode)

The fastest way to start a new React Router project is with the scaffolding CLI:

```bash
npx create-react-router@latest my-app
cd my-app
npm install
npm run dev
```

This creates a full-stack project with file-based routing, SSR, and the Vite dev server. Open `http://localhost:5173` to see your app.

### 1.2 Add to an Existing React App (Library Mode)

If you already have a React app and want to add routing:

```bash
npm install react-router
```

Then wrap your app with a router:

```tsx
import { BrowserRouter, Routes, Route, Link } from "react-router";

function App() {
  return (
    <BrowserRouter>
      <nav>
        <Link to="/">Home</Link>
        <Link to="/about">About</Link>
      </nav>
      <Routes>
        <Route path="/" element={<Home />} />
        <Route path="/about" element={<About />} />
      </Routes>
    </BrowserRouter>
  );
}

function Home() {
  return <h1>Home</h1>;
}

function About() {
  return <h1>About</h1>;
}
```

### 1.3 Data Router (Library Mode with Data APIs)

For apps that need loaders, actions, and data-driven navigation:

```tsx
import {
  createBrowserRouter,
  RouterProvider,
  Link,
  useLoaderData,
  Outlet,
} from "react-router";

const router = createBrowserRouter([
  {
    path: "/",
    element: <Root />,
    children: [
      {
        index: true,
        element: <Home />,
        loader: async () => {
          const res = await fetch("/api/dashboard");
          return res.json();
        },
      },
    ],
  },
]);

function Root() {
  return (
    <div>
      <nav>
        <Link to="/">Home</Link>
      </nav>
      <Outlet />
    </div>
  );
}

function Home() {
  const data = useLoaderData();
  return <h1>Welcome, {data.user.name}</h1>;
}

function App() {
  return <RouterProvider router={router} />;
}
```

> [!TIP]
> If you are starting a new project today, framework mode gives you the most capabilities out of the box — SSR, code splitting, type-safe routing, and data APIs all pre-configured. Use library mode when integrating into an existing app or when you need full control over your build pipeline.

---

## 2. Installation

### 2.1 System Requirements

| Requirement | Version |
|---|---|
| Node.js | >= 20.0.0 |
| React | 18 or 19 |
| Package manager | npm, pnpm, yarn, or bun |

### 2.2 Library Mode Installation

Install the core package:

```bash
# npm
npm install react-router

# pnpm
pnpm add react-router

# yarn
yarn add react-router

# bun
bun add react-router
```

> [!IMPORTANT]
> The `react-router-dom` package still exists but is a thin re-export shim. Import everything directly from `react-router`. The DOM-specific exports (like `RouterProvider` for the browser) are available from `react-router/dom` when needed.

### 2.3 Framework Mode Installation

Framework mode requires the Vite plugin and a server adapter:

```bash
# Core + framework
npm install react-router @react-router/dev

# Pick a server adapter (choose one)
npm install @react-router/serve       # Built-in production server
npm install @react-router/express     # Express adapter
npm install @react-router/node        # Node.js streams adapter
npm install @react-router/cloudflare  # Cloudflare Workers/Pages
npm install @react-router/architect   # AWS Architect
```

### 2.4 Vite Configuration (Framework Mode)

Framework mode uses Vite as its build tool. Create or update `vite.config.ts`:

```ts
import { reactRouter } from "@react-router/dev/vite";
import { defineConfig } from "vite";

export default defineConfig({
  plugins: [reactRouter()],
});
```

React Router supports Vite v5, v6, v7-beta, and v8.

### 2.5 TypeScript Setup

React Router ships with full TypeScript support. For framework mode, the route typegen automatically generates types for your routes:

```bash
# Generate route types (framework mode)
npx react-router typegen
```

Add the generated types to your `tsconfig.json`:

```json
{
  "compilerOptions": {
    "types": [".react-router/types"]
  }
}
```

### 2.6 Package Overview

| Package | Purpose | When to install |
|---|---|---|
| `react-router` | Core routing library | Always |
| `@react-router/dev` | Vite plugin + CLI | Framework mode |
| `@react-router/serve` | Production server | Framework mode (default server) |
| `@react-router/express` | Express adapter | Framework mode with Express |
| `@react-router/node` | Node.js adapter | Framework mode SSR with Node streams |
| `@react-router/cloudflare` | Cloudflare adapter | Framework mode on Cloudflare |
| `@react-router/architect` | AWS Architect adapter | Framework mode on AWS |
| `@react-router/fs-routes` | File-system routing convention | Framework mode with FS routing |
| `@react-router/remix-routes-option-adapter` | Migration adapter | Migrating from Remix |
| `create-react-router` | Project scaffolding | New projects |

---

## 3. Configuration

### 3.1 Framework Mode Configuration (`react-router.config.ts`)

Framework mode is configured via `react-router.config.ts` at the project root:

```ts
import type { Config } from "@react-router/dev/config";

export default {
  // App source directory (default: "app")
  appDirectory: "app",

  // URL basename (default: "/")
  basename: "/",

  // Build output directory (default: "build")
  buildDirectory: "build",

  // Server build filename (default: "index.js")
  serverBuildFile: "index.js",

  // Module format: "esm" or "cjs" (default: "esm")
  serverModuleFormat: "esm",

  // Enable SSR (default: true, set false for SPA mode)
  ssr: true,
} satisfies Config;
```

### 3.2 SPA Mode

Disable server-side rendering to build a single-page application:

```ts
import type { Config } from "@react-router/dev/config";

export default {
  ssr: false,
} satisfies Config;
```

In SPA mode, the build produces a static `index.html` and client-side JavaScript. No server runtime is required — you can deploy to any static hosting.

### 3.3 Prerendering

Generate static HTML at build time for specific routes:

```ts
import type { Config } from "@react-router/dev/config";

export default {
  // Prerender all routes
  prerender: true,

  // Or prerender specific URLs
  prerender: ["/", "/about", "/blog/intro"],

  // Or compute URLs dynamically
  prerender: async () => {
    const posts = await fetchBlogSlugs();
    return ["/", "/about", ...posts.map((p) => `/blog/${p.slug}`)];
  },
} satisfies Config;
```

> [!NOTE]
> Prerendering is compatible with SSR. Non-prerendered routes will still use server rendering at request time. This is useful for sites where some pages are static (landing, blog) and others are dynamic (dashboard, settings).

### 3.4 Lazy Route Discovery

React Router can lazily discover routes to reduce initial bundle size:

```ts
import type { Config } from "@react-router/dev/config";

export default {
  routeDiscovery: {
    // "lazy" = discover routes as user navigates (default)
    // "initial" = send all routes in the initial response
    mode: "lazy",
  },
} satisfies Config;
```

In lazy mode, the manifest for unvisited routes is fetched on demand when the user hovers or focuses a link pointing to an undiscovered route.

### 3.5 Server Bundles

Split your server code into multiple bundles for different deployment targets:

```ts
import type { Config } from "@react-router/dev/config";

export default {
  serverBundles({ branch }) {
    // Split into separate bundles based on route tree
    if (branch.some((route) => route.id.startsWith("routes/api"))) {
      return "api";
    }
    return "pages";
  },
} satisfies Config;
```

### 3.6 CSRF Protection

Restrict which origins can submit form data to your actions:

```ts
import type { Config } from "@react-router/dev/config";

export default {
  allowedActionOrigins: ["https://myapp.com", "https://admin.myapp.com"],
} satisfies Config;
```

### 3.7 Future Flags

Opt into upcoming breaking changes early:

```ts
import type { Config } from "@react-router/dev/config";

export default {
  future: {
    v8_middleware: true,           // Enable middleware support
    v8_splitRouteModules: true,    // Split route modules for better code splitting
    v8_viteEnvironmentApi: true,   // Use Vite environment API
    unstable_optimizeDeps: true,   // Pre-bundle dependencies
    unstable_passThroughRequests: true,  // Pass non-matched requests through
    unstable_subResourceIntegrity: true, // SRI for scripts/styles
    unstable_trailingSlashAwareDataRequests: true, // Trailing slash handling
  },
} satisfies Config;
```

### 3.8 Route Configuration (`routes.ts`)

In framework mode, define your routes in `app/routes.ts`:

```ts
import { route, index, layout, prefix } from "@react-router/dev/routes";

export default [
  // Index route at "/"
  index("routes/home.tsx"),

  // Regular route at "/about"
  route("about", "routes/about.tsx"),

  // Nested routes under "/dashboard"
  route("dashboard", "routes/dashboard/layout.tsx", [
    index("routes/dashboard/index.tsx"),
    route("settings", "routes/dashboard/settings.tsx"),
    route("profile", "routes/dashboard/profile.tsx"),
  ]),

  // Layout route (no URL segment, wraps children)
  layout("routes/auth-layout.tsx", [
    route("login", "routes/login.tsx"),
    route("register", "routes/register.tsx"),
  ]),

  // API routes with a prefix
  ...prefix("api", [
    route("users", "routes/api/users.tsx"),
    route("posts", "routes/api/posts.tsx"),
  ]),
];
```

#### Route Configuration API Reference

| Function | Description |
|---|---|
| `route(path, file, children?)` | Define a route with a URL path segment |
| `index(file)` | Define an index route (matches parent path exactly) |
| `layout(file, children?)` | Define a layout route (no URL segment, wraps children) |
| `prefix(path, children)` | Add a path prefix to a set of routes |
| `relative(dir, routes)` | Resolve route file paths relative to a directory |
| `getAppDirectory()` | Get the configured app directory path |

### 3.9 File-System Routing

Instead of manual route configuration, use the filesystem convention:

```bash
npm install @react-router/fs-routes
```

```ts
// app/routes.ts
import { flatRoutes } from "@react-router/fs-routes";

export default flatRoutes();
```

The filesystem convention uses the `app/routes/` directory:

| File | URL |
|---|---|
| `routes/_index.tsx` | `/` |
| `routes/about.tsx` | `/about` |
| `routes/blog.tsx` | `/blog` (layout) |
| `routes/blog._index.tsx` | `/blog` (index) |
| `routes/blog.$slug.tsx` | `/blog/:slug` |
| `routes/blog.($slug).tsx` | `/blog/:slug?` (optional) |
| `routes/files.$.tsx` | `/files/*` (splat) |
| `routes/docs.[sitemap.xml].tsx` | `/docs/sitemap.xml` (escaped) |
| `routes/_auth.tsx` | (pathless layout) |
| `routes/_auth.login.tsx` | `/login` (child of pathless layout) |

Key conventions:

- `$param` — dynamic URL segment
- `($param)` — optional dynamic segment
- `[literal]` — escaped literal (for dots, brackets)
- `_index` — index route
- `_` prefix — pathless layout (no URL segment)
- `.` — path separator (replaces `/`)
- Supported extensions: `.js`, `.jsx`, `.ts`, `.tsx`, `.md`, `.mdx`

### 3.10 Library Mode Configuration

Library mode (declarative or data router) requires no configuration file. You configure routing entirely in code. For the `BrowserRouter`, the `basename` prop serves the same purpose as the framework config option:

```tsx
<BrowserRouter basename="/app">
  <Routes>
    <Route path="/" element={<Home />} />
  </Routes>
</BrowserRouter>
```

### 3.11 Post-Build Hook

Run custom logic after the build completes:

```ts
import type { Config } from "@react-router/dev/config";

export default {
  async buildEnd({ buildManifest, reactRouterConfig, viteConfig }) {
    // Generate a sitemap, copy assets, notify a service, etc.
    await generateSitemap(buildManifest);
  },
} satisfies Config;
```

### 3.12 Plugin Presets

Extend configuration with reusable presets:

```ts
import type { Config } from "@react-router/dev/config";
import vercel from "@vercel/react-router/preset";

export default {
  presets: [vercel()],
} satisfies Config;
```

Presets can provide default configuration values, routes, and server adapters that are merged with your own config.

---

## 4. Core Usage

### 4.1 Understanding the Three Modes

React Router offers three progressively richer usage modes:

| Mode | Router | Route Definition | Data APIs | SSR |
|---|---|---|---|---|
| Declarative | `BrowserRouter` | JSX `<Route>` elements | No | No |
| Data Router | `createBrowserRouter` | Route objects | Yes | Manual |
| Framework | `@react-router/dev` | `routes.ts` or filesystem | Yes | Built-in |

Each mode builds on the previous one. Declarative mode provides components and hooks for URL-based rendering. Data Router mode adds loaders, actions, and pending states. Framework mode adds SSR, code splitting, and the build pipeline.

### 4.2 Declarative Routing

#### Basic Setup

```tsx
import { BrowserRouter, Routes, Route, Link } from "react-router";

function App() {
  return (
    <BrowserRouter>
      <nav>
        <Link to="/">Home</Link>
        <Link to="/products">Products</Link>
        <Link to="/about">About</Link>
      </nav>
      <Routes>
        <Route path="/" element={<Home />} />
        <Route path="/products" element={<Products />} />
        <Route path="/products/:id" element={<ProductDetail />} />
        <Route path="/about" element={<About />} />
        <Route path="*" element={<NotFound />} />
      </Routes>
    </BrowserRouter>
  );
}
```

#### Nested Routes

Nested routes allow you to compose layouts with child content areas. Parent routes render an `<Outlet />` where child routes appear:

```tsx
import { BrowserRouter, Routes, Route, Link, Outlet } from "react-router";

function App() {
  return (
    <BrowserRouter>
      <Routes>
        <Route path="/" element={<Layout />}>
          <Route index element={<Home />} />
          <Route path="dashboard" element={<Dashboard />}>
            <Route index element={<DashboardHome />} />
            <Route path="settings" element={<Settings />} />
            <Route path="profile" element={<Profile />} />
          </Route>
          <Route path="*" element={<NotFound />} />
        </Route>
      </Routes>
    </BrowserRouter>
  );
}

function Layout() {
  return (
    <div>
      <header>My App</header>
      {/* Child routes render here */}
      <Outlet />
      <footer>&copy; 2026</footer>
    </div>
  );
}

function Dashboard() {
  return (
    <div>
      <h1>Dashboard</h1>
      <nav>
        <Link to="">Overview</Link>
        <Link to="settings">Settings</Link>
        <Link to="profile">Profile</Link>
      </nav>
      <Outlet />
    </div>
  );
}
```

When the URL is `/dashboard/settings`, the render hierarchy is: `Layout` > `Dashboard` > `Settings`, with each parent rendering its `<Outlet />` where the child appears.

#### Index Routes

An index route renders when the parent route matches exactly (no further path segments):

```tsx
<Route path="dashboard" element={<Dashboard />}>
  {/* Renders at /dashboard (exact) */}
  <Route index element={<DashboardHome />} />
  {/* Renders at /dashboard/settings */}
  <Route path="settings" element={<Settings />} />
</Route>
```

#### Router Types

| Router | Use Case |
|---|---|
| `BrowserRouter` | Standard web apps (uses HTML5 history API) |
| `HashRouter` | When you cannot configure the server for URL rewrites |
| `MemoryRouter` | Testing, non-browser environments (React Native) |
| `StaticRouter` | Server-side rendering (manual SSR without framework mode) |

```tsx
// Hash-based routing (URLs like /#/about)
import { HashRouter, Routes, Route } from "react-router";

<HashRouter>
  <Routes>
    <Route path="/" element={<Home />} />
  </Routes>
</HashRouter>
```

```tsx
// Memory routing (for tests)
import { MemoryRouter } from "react-router";

<MemoryRouter initialEntries={["/dashboard"]}>
  <Routes>
    <Route path="/dashboard" element={<Dashboard />} />
  </Routes>
</MemoryRouter>
```

```tsx
// Static routing (SSR)
import { StaticRouter } from "react-router";

function handleRequest(req) {
  const html = renderToString(
    <StaticRouter location={req.url}>
      <App />
    </StaticRouter>
  );
  return html;
}
```

### 4.3 Data Router Mode

Data routers unlock loaders, actions, fetchers, and pending UI — the full data layer.

#### Creating a Data Router

```tsx
import {
  createBrowserRouter,
  RouterProvider,
  Outlet,
  Link,
  useLoaderData,
  useNavigation,
} from "react-router";

const router = createBrowserRouter([
  {
    path: "/",
    element: <Root />,
    errorElement: <ErrorBoundary />,
    children: [
      {
        index: true,
        element: <Home />,
        loader: homeLoader,
      },
      {
        path: "contacts/:contactId",
        element: <Contact />,
        loader: contactLoader,
        action: contactAction,
      },
    ],
  },
]);

function App() {
  return <RouterProvider router={router} />;
}
```

> [!NOTE]
> `createBrowserRouter` is for browser environments. For hash-based routing use `createHashRouter`, and for testing use `createMemoryRouter`. The API is identical across all three; only the history implementation differs.

#### Router Factory Functions

| Function | Environment |
|---|---|
| `createBrowserRouter(routes, opts?)` | Browser (HTML5 history) |
| `createHashRouter(routes, opts?)` | Browser (hash-based URLs) |
| `createMemoryRouter(routes, opts?)` | Testing, non-browser |
| `createStaticHandler(routes)` | SSR request handling |
| `createStaticRouter(handler, context)` | SSR rendering |

#### Loaders

Loaders fetch data before a route renders. They run on every navigation to that route:

```tsx
// Define the loader
async function contactLoader({ params, request }) {
  const response = await fetch(`/api/contacts/${params.contactId}`);
  if (!response.ok) {
    throw new Response("Contact not found", { status: 404 });
  }
  return response.json();
}

// Use the data in the component
function Contact() {
  const contact = useLoaderData();
  return (
    <div>
      <h1>{contact.name}</h1>
      <p>{contact.email}</p>
    </div>
  );
}

// Wire them together in the route
{
  path: "contacts/:contactId",
  element: <Contact />,
  loader: contactLoader,
}
```

Loaders receive two arguments:

| Argument | Contents |
|---|---|
| `params` | URL parameters from the route path (e.g., `{ contactId: "42" }`) |
| `request` | A standard `Request` object with the URL, headers, etc. |

#### Actions

Actions handle data mutations (form submissions, API calls that change state):

```tsx
async function contactAction({ request, params }) {
  const formData = await request.formData();
  const updates = Object.fromEntries(formData);

  const response = await fetch(`/api/contacts/${params.contactId}`, {
    method: "PUT",
    headers: { "Content-Type": "application/json" },
    body: JSON.stringify(updates),
  });

  if (!response.ok) {
    return { error: "Failed to update contact" };
  }

  // Returning a redirect navigates after the action completes
  return redirect(`/contacts/${params.contactId}`);
}

function EditContact() {
  const contact = useLoaderData();
  const actionData = useActionData();

  return (
    <Form method="post">
      {actionData?.error && <p className="error">{actionData.error}</p>}
      <input name="name" defaultValue={contact.name} />
      <input name="email" defaultValue={contact.email} />
      <button type="submit">Save</button>
    </Form>
  );
}
```

After an action completes, React Router automatically revalidates all active loaders so the UI stays in sync with the server.

#### Error Boundaries

```tsx
import { useRouteError, isRouteErrorResponse } from "react-router";

function ErrorBoundary() {
  const error = useRouteError();

  if (isRouteErrorResponse(error)) {
    return (
      <div>
        <h1>{error.status} {error.statusText}</h1>
        <p>{error.data}</p>
      </div>
    );
  }

  return (
    <div>
      <h1>Something went wrong</h1>
      <p>{error instanceof Error ? error.message : "Unknown error"}</p>
    </div>
  );
}

// Attach to a route
const router = createBrowserRouter([
  {
    path: "/",
    element: <Root />,
    errorElement: <ErrorBoundary />,
    children: [
      {
        path: "invoices/:id",
        element: <Invoice />,
        loader: invoiceLoader,
        // This error boundary catches errors specific to this route
        errorElement: <InvoiceError />,
      },
    ],
  },
]);
```

Error boundaries bubble up: if a child route throws and has no `errorElement`, the parent's error boundary catches it. This lets you set a single root error boundary as a fallback and add granular error handling only where needed.

### 4.4 Framework Mode

Framework mode provides the richest experience: file-based routing, SSR, automatic code splitting, type-safe routes, and integrated data loading.

#### Project Structure

```
my-app/
├── app/
│   ├── root.tsx          # Root layout (HTML shell)
│   ├── routes.ts         # Route definitions
│   └── routes/
│       ├── _index.tsx    # Home page (/)
│       ├── about.tsx     # /about
│       └── blog.$slug.tsx # /blog/:slug
├── react-router.config.ts
├── vite.config.ts
└── package.json
```

#### Root Layout (`app/root.tsx`)

The root layout defines the HTML shell for your application:

```tsx
import {
  Links,
  Meta,
  Outlet,
  Scripts,
  ScrollRestoration,
} from "react-router";

export function Layout({ children }: { children: React.ReactNode }) {
  return (
    <html lang="en">
      <head>
        <meta charSet="utf-8" />
        <meta name="viewport" content="width=device-width, initial-scale=1" />
        <Meta />
        <Links />
      </head>
      <body>
        {children}
        <ScrollRestoration />
        <Scripts />
      </body>
    </html>
  );
}

export default function Root() {
  return <Outlet />;
}
```

The SSR components in the root layout serve specific roles:

| Component | Purpose |
|---|---|
| `<Meta />` | Renders `<meta>` tags from route `meta` exports |
| `<Links />` | Renders `<link>` tags from route `links` exports |
| `<Scripts />` | Renders the client-side JavaScript bundles |
| `<ScrollRestoration />` | Restores scroll position on navigation |

#### Route Modules

Each route file can export several conventions:

```tsx
// app/routes/blog.$slug.tsx
import type { Route } from "./+types/blog.$slug";

// Metadata for <head>
export const meta: Route.MetaFunction = ({ data }) => {
  return [
    { title: data.post.title },
    { name: "description", content: data.post.excerpt },
  ];
};

// Link tags for <head>
export const links: Route.LinksFunction = () => {
  return [{ rel: "stylesheet", href: "/blog.css" }];
};

// HTTP response headers
export const headers: Route.HeadersFunction = () => {
  return {
    "Cache-Control": "public, max-age=3600",
  };
};

// Server-side data loading
export async function loader({ params }: Route.LoaderArgs) {
  const post = await db.posts.findBySlug(params.slug);
  if (!post) throw new Response("Not found", { status: 404 });
  return { post };
}

// Server-side mutation handling
export async function action({ request, params }: Route.ActionArgs) {
  const formData = await request.formData();
  await db.comments.create({
    postSlug: params.slug,
    body: formData.get("body") as string,
  });
  return { ok: true };
}

// Default export = the page component
export default function BlogPost({ loaderData }: Route.ComponentProps) {
  const { post } = loaderData;
  return (
    <article>
      <h1>{post.title}</h1>
      <div dangerouslySetInnerHTML={{ __html: post.html }} />
    </article>
  );
}

// Error boundary for this route
export function ErrorBoundary() {
  const error = useRouteError();
  return <div>Error loading post</div>;
}
```

#### Route Module Exports Summary

| Export | Purpose |
|---|---|
| `default` | The route component |
| `loader` | Server-side data loading (runs before render) |
| `action` | Server-side mutation handling (form submissions) |
| `clientLoader` | Client-side data loading |
| `clientAction` | Client-side mutation handling |
| `meta` | Document `<meta>` tags |
| `links` | Document `<link>` tags |
| `headers` | HTTP response headers |
| `handle` | Arbitrary data exposed via `useMatches` |
| `ErrorBoundary` | Error UI for this route |
| `HydrateFallback` | UI shown during client hydration |
| `shouldRevalidate` | Control when loaders re-run |

#### Client Loaders and Actions

Run loaders and actions on the client side (in addition to or instead of server):

```tsx
// Client-side loader (runs in the browser)
export async function clientLoader({ serverLoader }: Route.ClientLoaderArgs) {
  // Check cache first
  const cached = sessionStorage.getItem("dashboard-data");
  if (cached) return JSON.parse(cached);

  // Fall back to server loader
  const data = await serverLoader();
  sessionStorage.setItem("dashboard-data", JSON.stringify(data));
  return data;
}

// Tell React Router this route needs the client loader to hydrate
clientLoader.hydrate = true;

// Client-side action
export async function clientAction({
  request,
  serverAction,
}: Route.ClientActionArgs) {
  const formData = await request.formData();

  // Client-side validation
  if (!formData.get("email")) {
    return { error: "Email is required" };
  }

  // Forward to server
  return serverAction();
}
```

> [!TIP]
> Client loaders are powerful for caching strategies. Use them to implement stale-while-revalidate patterns, read from IndexedDB or localStorage, or skip the server entirely for data that only exists on the client.

### 4.5 Navigation Components

#### Link

The basic navigation component. Renders an `<a>` tag that performs client-side navigation:

```tsx
import { Link } from "react-router";

// Basic link
<Link to="/about">About</Link>

// Relative link (relative to current route)
<Link to="settings">Settings</Link>

// Link with state (accessible via useLocation().state)
<Link to="/login" state={{ from: "/dashboard" }}>Login</Link>

// Replace history entry (no back button)
<Link to="/welcome" replace>Welcome</Link>

// Prevent scroll reset on navigation
<Link to="/tab2" preventScrollReset>Tab 2</Link>

// Link with view transition animation
<Link to="/next" viewTransition>Next Page</Link>

// Relative to path (not route hierarchy)
<Link to=".." relative="path">Back</Link>
```

#### NavLink

An enhanced `Link` that knows whether it is active, pending, or transitioning:

```tsx
import { NavLink } from "react-router";

// className as function
<NavLink
  to="/dashboard"
  className={({ isActive, isPending, isTransitioning }) =>
    [
      isPending ? "pending" : "",
      isActive ? "active" : "",
      isTransitioning ? "transitioning" : "",
    ].join(" ")
  }
>
  Dashboard
</NavLink>

// style as function
<NavLink
  to="/messages"
  style={({ isActive }) => ({
    fontWeight: isActive ? "bold" : "normal",
    color: isActive ? "#1a73e8" : "inherit",
  })}
>
  Messages
</NavLink>

// Children as render function
<NavLink to="/tasks">
  {({ isActive }) => (
    <span>{isActive ? ">> Tasks" : "Tasks"}</span>
  )}
</NavLink>

// End prop — only match exact path, not descendant routes
<NavLink to="/dashboard" end>Dashboard Home</NavLink>
```

The `end` prop is important for parent routes. Without it, `<NavLink to="/dashboard">` would be active on `/dashboard/settings` too. With `end`, it is only active on exactly `/dashboard`.

#### Form

The `Form` component provides declarative data mutations. It works like a standard HTML `<form>` but intercepts the submission to perform client-side navigation and trigger route actions:

```tsx
import { Form } from "react-router";

// POST form (triggers route action)
<Form method="post">
  <input name="title" />
  <textarea name="body" />
  <button type="submit">Create Post</button>
</Form>

// GET form (navigates with query string)
<Form method="get" action="/search">
  <input name="q" placeholder="Search..." />
  <button type="submit">Search</button>
</Form>

// DELETE-style mutation (use POST with intent)
<Form method="post">
  <input type="hidden" name="intent" value="delete" />
  <input type="hidden" name="id" value={item.id} />
  <button type="submit">Delete</button>
</Form>

// File upload
<Form method="post" encType="multipart/form-data">
  <input type="file" name="avatar" />
  <button type="submit">Upload</button>
</Form>

// Navigate without reloading
<Form method="post" navigate={false}>
  {/* Submits without navigating — useful with useFetcher alternative */}
</Form>
```

#### Navigate

Redirect on render (component-based redirect):

```tsx
import { Navigate } from "react-router";

function ProtectedRoute({ children }) {
  const { user } = useAuth();
  if (!user) {
    return <Navigate to="/login" replace />;
  }
  return children;
}
```

#### Outlet

Renders the matching child route. Optionally pass context data to children:

```tsx
import { Outlet, useOutletContext } from "react-router";

function Dashboard() {
  const [user, setUser] = useState(null);

  return (
    <div>
      <Sidebar />
      {/* Pass context to child routes */}
      <Outlet context={{ user, setUser }} />
    </div>
  );
}

// In a child route:
function DashboardSettings() {
  // Type the context for TypeScript safety
  const { user, setUser } = useOutletContext<{
    user: User | null;
    setUser: (u: User) => void;
  }>();

  return <SettingsForm user={user} onSave={setUser} />;
}
```

#### ScrollRestoration

Restores scroll position when navigating back/forward. Place it once in your root layout (data router or framework mode only):

```tsx
import { ScrollRestoration } from "react-router";

// In your root layout
function Root() {
  return (
    <>
      <Outlet />
      <ScrollRestoration />
    </>
  );
}

// Custom scroll key — for example, scroll by pathname only, ignoring search params
<ScrollRestoration
  getKey={(location) => location.pathname}
/>

// Restore scroll per-element (not just window)
<ScrollRestoration
  getKey={(location) => {
    // Use different keys for different "tabs" at the same URL
    return location.pathname + location.search;
  }}
/>
```

#### Await

Render deferred data with a Suspense-compatible loading fallback:

```tsx
import { Await, useLoaderData } from "react-router";
import { Suspense } from "react";

export async function loader() {
  return {
    user: await getUser(),                    // Awaited before render
    recommendations: getRecommendations(),    // Promise passed through (deferred)
  };
}

export default function Dashboard() {
  const { user, recommendations } = useLoaderData();

  return (
    <div>
      <h1>Welcome, {user.name}</h1>
      <Suspense fallback={<p>Loading recommendations...</p>}>
        <Await resolve={recommendations}>
          {(items) => (
            <ul>
              {items.map((item) => (
                <li key={item.id}>{item.title}</li>
              ))}
            </ul>
          )}
        </Await>
      </Suspense>
    </div>
  );
}
```

The key insight: if you `await` a promise in the loader, the page waits for it before rendering. If you return the promise without `await`, the page renders immediately and the `<Await>` component streams in the data when it resolves.

### 4.6 Navigation Hooks

#### useNavigate

Programmatic navigation:

```tsx
import { useNavigate } from "react-router";

function LoginForm() {
  const navigate = useNavigate();

  async function handleSubmit(event) {
    event.preventDefault();
    const success = await login(/* ... */);
    if (success) {
      navigate("/dashboard");
    }
  }

  return <form onSubmit={handleSubmit}>...</form>;
}

// Navigation options
navigate("/settings");                    // Push new entry
navigate("/settings", { replace: true }); // Replace current entry
navigate(-1);                             // Go back
navigate(-2);                             // Go back 2 entries
navigate(1);                              // Go forward
navigate("/login", {
  state: { from: location.pathname },     // Pass state data
});
navigate("/new-page", {
  viewTransition: true,                   // Animate the transition
});
```

#### useLocation

Access the current location object:

```tsx
import { useLocation } from "react-router";

function Analytics() {
  const location = useLocation();

  useEffect(() => {
    trackPageView(location.pathname);
  }, [location]);

  return null;
}
```

The location object contains:

| Property | Example | Description |
|---|---|---|
| `pathname` | `"/products/42"` | Current URL path |
| `search` | `"?color=red"` | Query string (including `?`) |
| `hash` | `"#details"` | Hash fragment (including `#`) |
| `state` | `{ from: "/" }` | State passed via navigation |
| `key` | `"default"` | Unique key for this history entry |

#### useParams

Access dynamic URL parameters:

```tsx
import { useParams } from "react-router";

// Route: "/users/:userId/posts/:postId"
// URL: "/users/123/posts/456"

function Post() {
  const { userId, postId } = useParams();
  // userId === "123"
  // postId === "456"

  return <h1>Post {postId} by User {userId}</h1>;
}
```

All parameter values are strings. Parse numbers or other types explicitly:

```tsx
const { id } = useParams();
const numericId = Number(id); // Convert when needed
```

#### useSearchParams

Read and update query string parameters without a full navigation:

```tsx
import { useSearchParams } from "react-router";

function ProductList() {
  const [searchParams, setSearchParams] = useSearchParams();

  const category = searchParams.get("category");  // "shoes" or null
  const page = searchParams.get("page") || "1";

  function nextPage() {
    setSearchParams((prev) => {
      prev.set("page", String(Number(page) + 1));
      return prev;
    });
  }

  function setCategory(cat: string) {
    setSearchParams({ category: cat, page: "1" });
  }

  return (
    <div>
      <h1>Products{category ? `: ${category}` : ""} (Page {page})</h1>
      <button onClick={() => setCategory("shoes")}>Shoes</button>
      <button onClick={() => setCategory("hats")}>Hats</button>
      <button onClick={nextPage}>Next Page</button>
    </div>
  );
}
```

`useSearchParams` returns a tuple of `[URLSearchParams, SetURLSearchParams]`, following the same pattern as React's `useState`.

#### useNavigation

Track the state of any in-progress navigation:

```tsx
import { useNavigation } from "react-router";

function Root() {
  const navigation = useNavigation();

  return (
    <div>
      {navigation.state === "loading" && <GlobalSpinner />}
      {navigation.state === "submitting" && <p>Saving...</p>}
      <Outlet />
    </div>
  );
}
```

| Property | Type | Description |
|---|---|---|
| `state` | `"idle" \| "loading" \| "submitting"` | Current navigation state |
| `location` | `Location \| undefined` | Target location (when loading) |
| `formData` | `FormData \| undefined` | Submitted form data (when submitting) |
| `formAction` | `string \| undefined` | Action URL being submitted to |
| `formMethod` | `string \| undefined` | HTTP method of the submission |
| `formEncType` | `string \| undefined` | Encoding type of the submission |

#### useBlocker

Block navigation when the user has unsaved changes:

```tsx
import { useBlocker } from "react-router";

function EditForm() {
  const [isDirty, setIsDirty] = useState(false);
  const blocker = useBlocker(isDirty);

  return (
    <div>
      <form onChange={() => setIsDirty(true)}>
        <input name="title" />
        <button type="submit">Save</button>
      </form>

      {blocker.state === "blocked" && (
        <div className="modal">
          <p>You have unsaved changes. Leave anyway?</p>
          <button onClick={() => blocker.proceed()}>Leave</button>
          <button onClick={() => blocker.reset()}>Stay</button>
        </div>
      )}
    </div>
  );
}
```

The blocker has three states: `"unblocked"` (default), `"blocked"` (navigation attempted), and `"proceeding"` (user confirmed navigation).

#### useBeforeUnload

Warn before the user closes or reloads the browser tab:

```tsx
import { useBeforeUnload } from "react-router";
import { useCallback } from "react";

function EditForm() {
  const [isDirty, setIsDirty] = useState(false);

  useBeforeUnload(
    useCallback(
      (event) => {
        if (isDirty) {
          event.preventDefault();
        }
      },
      [isDirty]
    )
  );

  return <form onChange={() => setIsDirty(true)}>...</form>;
}
```

> [!WARNING]
> `useBeforeUnload` only handles browser tab close/reload. For in-app navigation blocking, use `useBlocker` instead. You typically want both for full coverage.

### 4.7 Data Hooks

#### useLoaderData

Access the data returned by the current route's loader:

```tsx
import { useLoaderData } from "react-router";

export async function loader() {
  const users = await db.users.findMany();
  return { users };
}

export default function Users() {
  const { users } = useLoaderData();
  return (
    <ul>
      {users.map((user) => (
        <li key={user.id}>{user.name}</li>
      ))}
    </ul>
  );
}
```

In framework mode with typegen, the return type is inferred automatically from your loader function.

#### useActionData

Access the data returned by the current route's action (after a form submission):

```tsx
import { useActionData, Form } from "react-router";

export async function action({ request }) {
  const formData = await request.formData();
  const email = formData.get("email") as string;

  const errors: Record<string, string> = {};
  if (!email) errors.email = "Email is required";
  else if (!email.includes("@")) errors.email = "Invalid email";

  if (Object.keys(errors).length > 0) {
    return { errors };
  }

  await createSubscriber(email);
  return { success: true };
}

export default function Newsletter() {
  const actionData = useActionData();

  return (
    <Form method="post">
      <label>
        Email
        <input name="email" type="email" />
      </label>
      {actionData?.errors?.email && (
        <p className="error">{actionData.errors.email}</p>
      )}
      {actionData?.success && <p className="success">Subscribed!</p>}
      <button type="submit">Subscribe</button>
    </Form>
  );
}
```

`useActionData` returns `undefined` on first render (no submission yet), then returns the action's return value after submission.

#### useRouteLoaderData

Access loader data from a parent or any ancestor route by its route ID:

```tsx
import { useRouteLoaderData } from "react-router";

// In any child route component:
function UserAvatar() {
  // Access the root route's loader data
  // In data router mode, the route ID is the path or an explicit `id` property
  const rootData = useRouteLoaderData("root");
  if (!rootData?.user) return null;
  return <img src={rootData.user.avatarUrl} alt={rootData.user.name} />;
}
```

This is useful for accessing global data (current user, feature flags, site config) loaded in a root or layout route from deeply nested children.

#### useFetcher

Interact with loaders and actions without causing a navigation. This is the workhorse for in-place updates, auto-save, background operations, and any mutation that should not change the URL:

```tsx
import { useFetcher } from "react-router";

function TodoItem({ todo }) {
  const fetcher = useFetcher();

  // Optimistic UI: use fetcher's submitted data if available
  const isComplete = fetcher.formData
    ? fetcher.formData.get("complete") === "true"
    : todo.complete;

  return (
    <li className={isComplete ? "completed" : ""}>
      <fetcher.Form method="post" action={`/todos/${todo.id}/toggle`}>
        <input type="hidden" name="complete" value={String(!todo.complete)} />
        <button type="submit" disabled={fetcher.state !== "idle"}>
          {isComplete ? "Done" : "Todo"}: {todo.title}
        </button>
      </fetcher.Form>
    </li>
  );
}
```

Load data without navigation:

```tsx
function UserSearch() {
  const fetcher = useFetcher();
  const [query, setQuery] = useState("");

  useEffect(() => {
    if (query.length > 2) {
      fetcher.load(`/api/users?q=${encodeURIComponent(query)}`);
    }
  }, [query]);

  return (
    <div>
      <input
        value={query}
        onChange={(e) => setQuery(e.target.value)}
        placeholder="Search users..."
      />
      {fetcher.state === "loading" && <Spinner />}
      {fetcher.data && (
        <ul>
          {fetcher.data.users.map((u) => (
            <li key={u.id}>{u.name}</li>
          ))}
        </ul>
      )}
    </div>
  );
}
```

| Fetcher property | Description |
|---|---|
| `fetcher.state` | `"idle"`, `"loading"`, or `"submitting"` |
| `fetcher.data` | The data returned by the loader or action |
| `fetcher.formData` | The FormData being submitted |
| `fetcher.formAction` | The action URL |
| `fetcher.formMethod` | The HTTP method |
| `fetcher.Form` | A `<Form>` component bound to this fetcher |
| `fetcher.submit()` | Programmatic submit function |
| `fetcher.load(url)` | Load data from a route's loader |

#### useFetchers

Access all active fetchers (useful for global pending indicators):

```tsx
import { useFetchers } from "react-router";

function GlobalPendingIndicator() {
  const fetchers = useFetchers();
  const isSubmitting = fetchers.some((f) => f.state === "submitting");
  const isLoading = fetchers.some((f) => f.state === "loading");

  if (!isSubmitting && !isLoading) return null;
  return <div className="global-spinner" />;
}
```

#### useRevalidator

Manually trigger revalidation of all loader data:

```tsx
import { useRevalidator } from "react-router";

function RefreshButton() {
  const revalidator = useRevalidator();

  return (
    <button
      onClick={() => revalidator.revalidate()}
      disabled={revalidator.state === "loading"}
    >
      {revalidator.state === "loading" ? "Refreshing..." : "Refresh Data"}
    </button>
  );
}
```

Common use cases for manual revalidation:
- Polling on an interval
- After a WebSocket message indicates server data changed
- After a user returns to the tab (visibility change)

#### useSubmit

Submit forms programmatically:

```tsx
import { useSubmit } from "react-router";

// Auto-save on change
function AutoSaveForm() {
  const submit = useSubmit();

  function handleChange(event) {
    // Submit the entire form on any change
    submit(event.currentTarget.form, {
      method: "post",
      replace: true,
    });
  }

  return (
    <Form method="post">
      <textarea name="content" onChange={handleChange} />
    </Form>
  );
}

// Submit arbitrary data (not from a form)
function DeleteButton({ id }) {
  const submit = useSubmit();

  function handleDelete() {
    if (confirm("Are you sure?")) {
      submit(
        { intent: "delete", id },
        { method: "post", action: "/items" }
      );
    }
  }

  return <button onClick={handleDelete}>Delete</button>;
}

// Submit FormData directly
function FileUpload() {
  const submit = useSubmit();

  function handleFile(event) {
    const formData = new FormData();
    formData.set("file", event.target.files[0]);
    submit(formData, {
      method: "post",
      encType: "multipart/form-data",
    });
  }

  return <input type="file" onChange={handleFile} />;
}
```

### 4.8 URL and Path Utilities

#### useHref

Resolve a `to` value into a full href string:

```tsx
import { useHref } from "react-router";

function ExternalForm() {
  // Resolves against the current basename
  const href = useHref("/submit");
  return <form action={href} method="post">...</form>;
}
```

#### useResolvedPath

Resolve a relative path against the current location:

```tsx
import { useResolvedPath } from "react-router";

function Breadcrumb() {
  const resolved = useResolvedPath("..");
  // resolved.pathname — parent route's path
  return <Link to={resolved}>Back</Link>;
}
```

#### useMatch

Check if the current URL matches a specific pattern:

```tsx
import { useMatch } from "react-router";

function AdminBadge() {
  const match = useMatch("/admin/*");
  if (!match) return null;
  return <span className="badge">Admin Area</span>;
}

// Access captured params
function UserBanner() {
  const match = useMatch("/users/:id");
  if (!match) return null;
  return <p>Viewing user {match.params.id}</p>;
}
```

#### useMatches

Get all matched routes in the current hierarchy. Each match includes the route's `id`, `pathname`, `params`, `data` (loader data), and `handle`:

```tsx
import { useMatches } from "react-router";

function Breadcrumbs() {
  const matches = useMatches();

  return (
    <nav aria-label="breadcrumb">
      <ol>
        {matches
          .filter((match) => match.handle?.breadcrumb)
          .map((match) => (
            <Link key={match.id} to={match.pathname}>
              {typeof match.handle.breadcrumb === "function"
                ? match.handle.breadcrumb(match)
                : match.handle.breadcrumb}
            </Link>
          ))}
      </ol>
    </nav>
  );
}
```

#### useFormAction

Resolve the action URL for the current route:

```tsx
import { useFormAction } from "react-router";

function InlineAction() {
  const action = useFormAction();
  // Resolves to the current route's action URL
  // Useful for constructing action URLs in nested routes
  return <Form method="post" action={action}>...</Form>;
}
```

#### useLinkClickHandler

Build custom navigation components with full control over click handling:

```tsx
import { useLinkClickHandler } from "react-router";

function TrackedLink({ to, children, ...props }) {
  const handleClick = useLinkClickHandler(to);

  return (
    <a
      href={to}
      onClick={(event) => {
        analytics.track("link_click", { to });
        handleClick(event);
      }}
      {...props}
    >
      {children}
    </a>
  );
}
```

### 4.9 View Transitions

React Router integrates with the browser's View Transitions API for animated page transitions:

```tsx
import { Link, NavLink, useNavigate, useViewTransitionState } from "react-router";

// Enable via Link
<Link to="/gallery/42" viewTransition>View Photo</Link>

// Enable via NavLink
<NavLink to="/page" viewTransition>Page</NavLink>

// Enable programmatically
const navigate = useNavigate();
navigate("/next", { viewTransition: true });

// Check transition state for CSS animations
function ImageCard({ id, src }) {
  const isTransitioning = useViewTransitionState(`/gallery/${id}`);

  return (
    <Link to={`/gallery/${id}`} viewTransition>
      <img
        src={src}
        style={{
          viewTransitionName: isTransitioning ? "hero-image" : "",
        }}
      />
    </Link>
  );
}
```

Style your transitions with CSS:

```css
::view-transition-old(hero-image) {
  animation: scale-down 0.3s ease-out;
}

::view-transition-new(hero-image) {
  animation: scale-up 0.3s ease-in;
}

@keyframes scale-down {
  from { transform: scale(1); }
  to { transform: scale(0.8); opacity: 0; }
}

@keyframes scale-up {
  from { transform: scale(0.8); opacity: 0; }
  to { transform: scale(1); }
}
```

### 4.10 Type-Safe Routing (Framework Mode)

Framework mode generates TypeScript types for every route. Run `npx react-router typegen` (or configure it to run automatically during development) and import the generated types:

```tsx
// app/routes/products.$id.tsx
import type { Route } from "./+types/products.$id";

export async function loader({ params }: Route.LoaderArgs) {
  // params.id is typed as string (never undefined, since it's required)
  const product = await getProduct(params.id);
  return { product };
}

export async function action({ request }: Route.ActionArgs) {
  const formData = await request.formData();
  // Handle mutation
  return { success: true };
}

export default function Product({ loaderData, actionData }: Route.ComponentProps) {
  // loaderData.product is fully typed based on loader return type
  // actionData is typed based on action return type (or undefined)
  return <h1>{loaderData.product.name}</h1>;
}
```

The `href()` utility provides type-safe path construction:

```tsx
import { href } from "react-router";

// TypeScript errors if the route pattern or params are wrong
<Link to={href("/products/:id", { id: "42" })}>Product 42</Link>

// No params needed for static routes
<Link to={href("/about")}>About</Link>

// Optional params
<Link to={href("/blog/:slug?", { slug: "intro" })}>Blog Post</Link>
<Link to={href("/blog/:slug?")}>Blog Index</Link>
```

### 4.11 Standalone Utility Functions

These functions work outside of React components — in loaders, actions, tests, or plain JavaScript.

#### Path Matching and Generation

```tsx
import { matchPath, matchRoutes, generatePath } from "react-router";

// Match a single path pattern against a URL
const match = matchPath("/users/:id", "/users/42");
if (match) {
  console.log(match.params.id); // "42"
  console.log(match.pathname);  // "/users/42"
  console.log(match.pattern);   // { path: "/users/:id" }
}

// Match with options
matchPath({ path: "/users/:id", end: false }, "/users/42/posts");
// Matches (end: false allows trailing segments)

// Match against a route tree
const routes = [
  {
    path: "/",
    children: [
      { path: "users/:id" },
      { path: "posts/:postId" },
    ],
  },
];
const matches = matchRoutes(routes, "/users/42");
// Returns array of matched route objects with params

// Generate a path from a pattern + params
const path = generatePath("/users/:id/posts/:postId", {
  id: "42",
  postId: "7",
});
// Returns "/users/42/posts/7"

// Splat params
generatePath("/files/*", { "*": "docs/readme.md" });
// Returns "/files/docs/readme.md"
```

#### Path Manipulation

```tsx
import { createPath, parsePath, resolvePath } from "react-router";

// Create a path string from parts
createPath({
  pathname: "/users",
  search: "?page=2",
  hash: "#top",
});
// Returns "/users?page=2#top"

// Parse a path string into parts
parsePath("/users?page=2#top");
// Returns { pathname: "/users", search: "?page=2", hash: "#top" }

// Resolve a relative path against a base
resolvePath("settings", "/dashboard");
// Returns { pathname: "/dashboard/settings", search: "", hash: "" }

resolvePath("..", "/dashboard/settings");
// Returns { pathname: "/dashboard", search: "", hash: "" }
```

#### Response Helpers

Use these in loaders and actions to create standardized responses:

```tsx
import { redirect, redirectDocument, replace, data } from "react-router";

// Client-side redirect (SPA navigation)
export async function action() {
  await saveData();
  return redirect("/success");
}

// Redirect with status code
return redirect("/login", 302);

// Redirect with headers
return redirect("/dashboard", {
  headers: { "Set-Cookie": await commitSession(session) },
});

// Full document redirect (bypasses client-side routing, full page reload)
export async function action() {
  return redirectDocument("/external-page");
}

// Replace current history entry (navigates but no new history entry)
export async function action() {
  return replace("/dashboard");
}

// Typed response with custom status and headers
export async function loader() {
  return data(
    { items: await getItems() },
    {
      status: 200,
      headers: {
        "Cache-Control": "public, max-age=3600",
        "X-Custom-Header": "value",
      },
    }
  );
}

// Error responses (caught by error boundaries)
export async function loader({ params }) {
  const item = await getItem(params.id);
  if (!item) {
    throw data(
      { message: "Item not found" },
      { status: 404 }
    );
  }
  return { item };
}
```

#### Route Conversion Utilities

```tsx
import { createRoutesFromElements } from "react-router";

// Convert JSX route elements to route objects
// Useful if you have JSX routes but need to pass them to createBrowserRouter
const routes = createRoutesFromElements(
  <Route path="/" element={<Root />}>
    <Route index element={<Home />} />
    <Route path="about" element={<About />} />
  </Route>
);

const router = createBrowserRouter(routes);
```

#### Search Params Factory

```tsx
import { createSearchParams } from "react-router";

// From an object
const params = createSearchParams({ page: "1", sort: "name" });
params.toString(); // "page=1&sort=name"

// From entries array (supports repeated keys)
const tags = createSearchParams([
  ["tag", "react"],
  ["tag", "router"],
  ["tag", "typescript"],
]);
tags.toString(); // "tag=react&tag=router&tag=typescript"

// Use with navigate
const navigate = useNavigate();
navigate({
  pathname: "/search",
  search: createSearchParams({ q: "react router" }).toString(),
});
```

### 4.12 Server-Side Utilities (Framework Mode)

#### Request Handler

Create a request handler for your server adapter:

```tsx
import { createRequestHandler } from "react-router";

// Express adapter
import express from "express";

const app = express();

app.all(
  "*",
  createRequestHandler({
    build: () => import("./build/server/index.js"),
  })
);

app.listen(3000);
```

#### Session Management

React Router provides session utilities for server-side session handling:

```tsx
import {
  createCookieSessionStorage,
  createCookie,
  redirect,
} from "react-router";

// Create a session storage backed by cookies
const { getSession, commitSession, destroySession } =
  createCookieSessionStorage({
    cookie: {
      name: "__session",
      httpOnly: true,
      maxAge: 60 * 60 * 24 * 7, // 1 week
      path: "/",
      sameSite: "lax",
      secrets: [process.env.SESSION_SECRET],
      secure: process.env.NODE_ENV === "production",
    },
  });

// Use in a loader
export async function loader({ request }) {
  const session = await getSession(request.headers.get("Cookie"));
  const userId = session.get("userId");
  if (!userId) throw redirect("/login");

  const user = await getUser(userId);
  return { user };
}

// Use in an action (login)
export async function action({ request }) {
  const formData = await request.formData();
  const user = await authenticate(
    formData.get("email"),
    formData.get("password")
  );

  if (!user) {
    return { error: "Invalid credentials" };
  }

  const session = await getSession(request.headers.get("Cookie"));
  session.set("userId", user.id);

  return redirect("/dashboard", {
    headers: {
      "Set-Cookie": await commitSession(session),
    },
  });
}

// Logout action
export async function action({ request }) {
  const session = await getSession(request.headers.get("Cookie"));
  return redirect("/login", {
    headers: {
      "Set-Cookie": await destroySession(session),
    },
  });
}
```

Available session storage implementations:

| Storage | Use Case |
|---|---|
| `createCookieSessionStorage` | Store session data in encrypted cookies |
| `createMemorySessionStorage` | In-memory storage (development, testing) |
| `createSessionStorage` | Custom backend (database, Redis, file system) |

#### Flash Messages with Sessions

```tsx
export async function action({ request }) {
  const session = await getSession(request.headers.get("Cookie"));

  // Flash messages are removed after first read
  session.flash("success", "Profile updated successfully");

  return redirect("/profile", {
    headers: {
      "Set-Cookie": await commitSession(session),
    },
  });
}

export async function loader({ request }) {
  const session = await getSession(request.headers.get("Cookie"));
  const success = session.get("success"); // Reads and removes the flash

  return data(
    { success },
    {
      headers: {
        "Set-Cookie": await commitSession(session),
      },
    }
  );
}
```

#### Standalone Cookies

For simpler use cases that do not need full session management:

```tsx
import { createCookie } from "react-router";

const userPrefs = createCookie("prefs", {
  maxAge: 60 * 60 * 24 * 365, // 1 year
});

// Read
export async function loader({ request }) {
  const prefs = await userPrefs.parse(request.headers.get("Cookie"));
  return { theme: prefs?.theme || "light" };
}

// Write
export async function action({ request }) {
  const formData = await request.formData();
  const prefs = { theme: formData.get("theme") };
  return redirect("/settings", {
    headers: {
      "Set-Cookie": await userPrefs.serialize(prefs),
    },
  });
}
```

### 4.13 Middleware (v8 Future Flag)

> [!WARNING]
> Middleware requires the `v8_middleware` future flag and its API may change before the v8 stable release. Enable it with `future: { v8_middleware: true }` in `react-router.config.ts`.

```tsx
import { createContext } from "react-router";

// Create a typed context key
type AuthContext = { userId: string; role: string };
const authContext = createContext<AuthContext>();

// Define middleware in a route module
export const middleware = [
  async function auth({ request, context }) {
    const session = await getSession(request.headers.get("Cookie"));
    const userId = session.get("userId");
    if (!userId) throw redirect("/login");

    const user = await getUser(userId);
    context.set(authContext, { userId: user.id, role: user.role });
  },
];

// Access context in loaders and actions
export async function loader({ context }) {
  const { userId, role } = context.get(authContext);
  if (role !== "admin") throw new Response("Forbidden", { status: 403 });
  return await getAdminData(userId);
}
```

Middleware runs in order before the route's loader/action. Use `RouterContextProvider` in the root to make context available throughout the app:

```tsx
import { RouterContextProvider } from "react-router";

export default function Root() {
  return (
    <RouterContextProvider>
      <Outlet />
    </RouterContextProvider>
  );
}
```

---

## 5. Common Tasks

### 5.1 Authentication and Protected Routes

#### Redirect Unauthenticated Users (Data Router / Framework Mode)

```tsx
// In a layout route loader that wraps protected routes
export async function loader({ request }) {
  const user = await getUser(request);
  if (!user) {
    const url = new URL(request.url);
    throw redirect(`/login?returnTo=${encodeURIComponent(url.pathname)}`);
  }
  return { user };
}
```

#### Redirect Unauthenticated Users (Declarative Mode)

```tsx
function RequireAuth({ children }) {
  const { user } = useAuth();
  const location = useLocation();

  if (!user) {
    return <Navigate to="/login" state={{ from: location }} replace />;
  }

  return children;
}

// Usage in routes
<Route
  path="/dashboard/*"
  element={
    <RequireAuth>
      <Dashboard />
    </RequireAuth>
  }
/>
```

#### Redirect After Login

```tsx
// Login action (framework/data router)
export async function action({ request }) {
  const formData = await request.formData();
  const user = await authenticate(
    formData.get("email"),
    formData.get("password")
  );

  if (!user) {
    return { error: "Invalid credentials" };
  }

  const url = new URL(request.url);
  const returnTo = url.searchParams.get("returnTo") || "/dashboard";

  const session = await getSession(request.headers.get("Cookie"));
  session.set("userId", user.id);

  return redirect(returnTo, {
    headers: { "Set-Cookie": await commitSession(session) },
  });
}
```

#### Role-Based Access Control

```tsx
export async function loader({ request }) {
  const user = await requireUser(request);

  if (!user.roles.includes("admin")) {
    throw new Response("Forbidden", { status: 403 });
  }

  return { adminData: await getAdminDashboard() };
}
```

### 5.2 Code Splitting with Lazy Routes

#### Data Router Lazy Loading

```tsx
const router = createBrowserRouter([
  {
    path: "/",
    element: <Root />,
    children: [
      {
        path: "dashboard",
        lazy: async () => {
          const { Dashboard, loader, action } = await import("./routes/dashboard");
          return { Component: Dashboard, loader, action };
        },
      },
      {
        path: "settings",
        // If the module has default exports matching the route API,
        // they are picked up automatically
        lazy: () => import("./routes/settings"),
      },
    ],
  },
]);
```

The `lazy` function can return any route property except `path`, `index`, `children`, and `id`. Common properties to return: `Component`, `loader`, `action`, `errorElement`, `handle`, `shouldRevalidate`.

#### Declarative Lazy Loading

```tsx
import { lazy, Suspense } from "react";

const Dashboard = lazy(() => import("./pages/Dashboard"));
const Settings = lazy(() => import("./pages/Settings"));

function App() {
  return (
    <BrowserRouter>
      <Suspense fallback={<div>Loading...</div>}>
        <Routes>
          <Route path="/dashboard" element={<Dashboard />} />
          <Route path="/settings" element={<Settings />} />
        </Routes>
      </Suspense>
    </BrowserRouter>
  );
}
```

> [!TIP]
> In framework mode, code splitting is automatic. Each route module is compiled into its own chunk and loaded on demand during navigation. No manual `lazy()` calls are needed.

### 5.3 Search and Filtering

Implement search with GET forms and loaders for URL-driven, bookmarkable, shareable searches:

```tsx
import { Form, useLoaderData, useNavigation, useSearchParams } from "react-router";

export async function loader({ request }) {
  const url = new URL(request.url);
  const q = url.searchParams.get("q") || "";
  const category = url.searchParams.get("category") || "all";
  const page = Number(url.searchParams.get("page") || "1");

  const { products, total } = await searchProducts({ q, category, page });
  return { products, total, q, category, page };
}

export default function Search() {
  const { products, total, q, category, page } = useLoaderData();
  const navigation = useNavigation();
  const [searchParams, setSearchParams] = useSearchParams();

  const isSearching = navigation.state === "loading";

  return (
    <div>
      {/* GET form — submits to the current URL with query params */}
      <Form method="get">
        <input name="q" defaultValue={q} placeholder="Search products..." />
        <select name="category" defaultValue={category}>
          <option value="all">All Categories</option>
          <option value="electronics">Electronics</option>
          <option value="clothing">Clothing</option>
          <option value="books">Books</option>
        </select>
        <button type="submit">Search</button>
      </Form>

      <div className={isSearching ? "loading" : ""}>
        <p>{total} results</p>
        {products.map((product) => (
          <ProductCard key={product.id} product={product} />
        ))}
      </div>

      {/* Pagination */}
      <button
        disabled={page <= 1}
        onClick={() => {
          setSearchParams((prev) => {
            prev.set("page", String(page - 1));
            return prev;
          });
        }}
      >
        Previous
      </button>
      <button
        onClick={() => {
          setSearchParams((prev) => {
            prev.set("page", String(page + 1));
            return prev;
          });
        }}
      >
        Next
      </button>
    </div>
  );
}
```

### 5.4 Optimistic UI

Update the UI immediately before the server responds, then reconcile when the response arrives:

```tsx
function FavoriteButton({ item }) {
  const fetcher = useFetcher();

  // Use the optimistic value while the request is in flight
  const isFavorite = fetcher.formData
    ? fetcher.formData.get("favorite") === "true"
    : item.isFavorite;

  return (
    <fetcher.Form method="post" action="/api/favorites">
      <input type="hidden" name="itemId" value={item.id} />
      <input type="hidden" name="favorite" value={String(!isFavorite)} />
      <button
        type="submit"
        aria-label={isFavorite ? "Remove from favorites" : "Add to favorites"}
      >
        {isFavorite ? "Remove Favorite" : "Add Favorite"}
      </button>
    </fetcher.Form>
  );
}
```

#### Optimistic List Updates

```tsx
function TodoList() {
  const { todos } = useLoaderData();
  const fetchers = useFetchers();

  // Merge optimistic updates into the list
  const optimisticTodos = todos.map((todo) => {
    const fetcher = fetchers.find(
      (f) => f.formData?.get("todoId") === todo.id
    );
    if (fetcher?.formData) {
      return {
        ...todo,
        title: fetcher.formData.get("title") || todo.title,
        complete: fetcher.formData.get("complete") === "true",
      };
    }
    return todo;
  });

  return (
    <ul>
      {optimisticTodos.map((todo) => (
        <TodoItem key={todo.id} todo={todo} />
      ))}
    </ul>
  );
}
```

### 5.5 Pending UI (Loading and Submitting States)

#### Global Loading Indicator

```tsx
import { useNavigation } from "react-router";

function Root() {
  const navigation = useNavigation();

  return (
    <div>
      {navigation.state !== "idle" && (
        <div className="progress-bar" aria-label="Loading" />
      )}
      <Outlet />
    </div>
  );
}
```

#### Busy Submit Button

```tsx
function SubmitButton({ label = "Save" }) {
  const navigation = useNavigation();
  const isSubmitting = navigation.state === "submitting";

  return (
    <button type="submit" disabled={isSubmitting}>
      {isSubmitting ? "Saving..." : label}
    </button>
  );
}
```

#### Skeleton UI While Loading a Specific Route

```tsx
function DashboardLink() {
  const navigation = useNavigation();

  const isLoadingDashboard =
    navigation.state === "loading" &&
    navigation.location?.pathname === "/dashboard";

  return (
    <div>
      <Link to="/dashboard">Dashboard</Link>
      {isLoadingDashboard && <Spinner />}
    </div>
  );
}
```

### 5.6 Error Handling Patterns

#### Granular Error Boundaries

Place error boundaries at different levels for targeted error recovery:

```tsx
const router = createBrowserRouter([
  {
    path: "/",
    element: <Root />,
    errorElement: <RootError />,        // Last resort
    children: [
      {
        path: "dashboard",
        element: <Dashboard />,
        errorElement: <DashboardError />, // Dashboard-specific recovery
        children: [
          {
            path: "analytics",
            element: <Analytics />,
            // No errorElement — errors bubble up to DashboardError
          },
          {
            path: "billing",
            element: <Billing />,
            errorElement: <BillingError />, // Billing-specific recovery
          },
        ],
      },
    ],
  },
]);
```

#### Throwing Responses for HTTP Errors

```tsx
export async function loader({ params }) {
  const user = await getUser(params.id);
  if (!user) {
    throw new Response("User not found", { status: 404 });
  }
  if (!user.isPublic) {
    throw new Response("Forbidden", { status: 403 });
  }
  return { user };
}

// Error boundary with HTTP status handling
function ErrorBoundary() {
  const error = useRouteError();

  if (isRouteErrorResponse(error)) {
    switch (error.status) {
      case 404:
        return (
          <div>
            <h1>Not Found</h1>
            <p>The resource you requested does not exist.</p>
            <Link to="/">Go Home</Link>
          </div>
        );
      case 403:
        return (
          <div>
            <h1>Access Denied</h1>
            <p>You do not have permission to view this page.</p>
          </div>
        );
      default:
        return (
          <div>
            <h1>Error {error.status}</h1>
            <p>{error.statusText}</p>
          </div>
        );
    }
  }

  // Non-Response errors (runtime errors, network failures)
  return (
    <div>
      <h1>Something went wrong</h1>
      <p>{error instanceof Error ? error.message : "Unknown error"}</p>
    </div>
  );
}
```

#### Retry After Error

```tsx
function ErrorWithRetry() {
  const error = useRouteError();
  const revalidator = useRevalidator();

  return (
    <div>
      <h1>Failed to load data</h1>
      <p>{error instanceof Error ? error.message : "Unknown error"}</p>
      <button onClick={() => revalidator.revalidate()}>
        Try Again
      </button>
    </div>
  );
}
```

### 5.7 Modals as Routes

Render modals with shareable URLs while keeping the background page visible:

```tsx
function App() {
  const location = useLocation();
  const state = location.state;

  return (
    <>
      {/* Always render the background page */}
      <Routes location={state?.backgroundLocation || location}>
        <Route path="/" element={<Layout />}>
          <Route index element={<PhotoGrid />} />
          <Route path="photos/:id" element={<PhotoPage />} />
        </Route>
      </Routes>

      {/* Render the modal overlay when navigated from within the app */}
      {state?.backgroundLocation && (
        <Routes>
          <Route path="photos/:id" element={<PhotoModal />} />
        </Routes>
      )}
    </>
  );
}

// Grid links that open modals
function PhotoGrid() {
  const location = useLocation();

  return (
    <div className="grid">
      {photos.map((photo) => (
        <Link
          key={photo.id}
          to={`/photos/${photo.id}`}
          state={{ backgroundLocation: location }}
        >
          <img src={photo.thumbnail} alt={photo.alt} />
        </Link>
      ))}
    </div>
  );
}

// Modal component
function PhotoModal() {
  const { id } = useParams();
  const navigate = useNavigate();

  return (
    <div className="modal-overlay" onClick={() => navigate(-1)}>
      <div className="modal-content" onClick={(e) => e.stopPropagation()}>
        <img src={`/photos/${id}/full.jpg`} alt="" />
        <button onClick={() => navigate(-1)}>Close</button>
      </div>
    </div>
  );
}
```

When the user navigates directly to `/photos/42` (e.g., from a bookmark), there is no `backgroundLocation` in state, so `PhotoPage` renders as a full page instead of a modal.

### 5.8 Breadcrumbs

Build breadcrumbs using the `handle` property and `useMatches`:

```tsx
// Define breadcrumbs in route configuration
const router = createBrowserRouter([
  {
    id: "root",
    path: "/",
    element: <Root />,
    handle: { breadcrumb: "Home" },
    children: [
      {
        path: "products",
        element: <Products />,
        handle: { breadcrumb: "Products" },
        children: [
          {
            path: ":id",
            element: <ProductDetail />,
            loader: productLoader,
            handle: {
              breadcrumb: (match) => match.data?.product?.name || "Product",
            },
          },
        ],
      },
    ],
  },
]);

// Breadcrumb component
function Breadcrumbs() {
  const matches = useMatches();
  const crumbs = matches.filter((m) => m.handle?.breadcrumb);

  return (
    <nav aria-label="Breadcrumb">
      <ol className="breadcrumbs">
        {crumbs.map((match, index) => {
          const label =
            typeof match.handle.breadcrumb === "function"
              ? match.handle.breadcrumb(match)
              : match.handle.breadcrumb;

          const isLast = index === crumbs.length - 1;

          return (
            <li key={match.id}>
              {isLast ? (
                <span aria-current="page">{label}</span>
              ) : (
                <Link to={match.pathname}>{label}</Link>
              )}
            </li>
          );
        })}
      </ol>
    </nav>
  );
}
```

### 5.9 Dynamic Route Parameters

#### Required Parameters

```tsx
// Route: /users/:userId
// URL:   /users/123

function UserProfile() {
  const { userId } = useParams();
  // userId === "123" (always a string)
  return <h1>User {userId}</h1>;
}
```

#### Optional Parameters (Framework Mode FS Routing)

```tsx
// File: routes/blog.($slug).tsx
// Matches: /blog (no slug) AND /blog/my-post (with slug)

export default function Blog() {
  const { slug } = useParams();

  if (!slug) {
    return <BlogIndex />;
  }

  return <BlogPost slug={slug} />;
}
```

#### Splat (Catch-All) Parameters

```tsx
// Route: /files/*
// URL:   /files/documents/reports/q4.pdf

function FileViewer() {
  const { "*": filepath } = useParams();
  // filepath === "documents/reports/q4.pdf"

  return <FileDisplay path={filepath} />;
}
```

#### Multiple Dynamic Segments

```tsx
// Route: /org/:orgId/team/:teamId/member/:memberId

function MemberProfile() {
  const { orgId, teamId, memberId } = useParams();
  // All three are available as strings
  return (
    <p>
      Org {orgId}, Team {teamId}, Member {memberId}
    </p>
  );
}
```

### 5.10 Meta and Links (Framework Mode)

#### Setting Page Metadata

```tsx
import type { Route } from "./+types/my-route";

export const meta: Route.MetaFunction = ({ data, params, location }) => {
  return [
    { title: `${data.product.name} | My Store` },
    { name: "description", content: data.product.description },
    { property: "og:title", content: data.product.name },
    { property: "og:image", content: data.product.image },
    { property: "og:url", content: `https://mystore.com${location.pathname}` },
    {
      "script:ld+json": {
        "@context": "https://schema.org",
        "@type": "Product",
        name: data.product.name,
        description: data.product.description,
      },
    },
  ];
};
```

Meta from child routes replaces parent route meta by default. To merge parent meta:

```tsx
export const meta: Route.MetaFunction = ({ data, matches }) => {
  // Get parent meta
  const parentMeta = matches.flatMap((match) => match.meta ?? []);
  return [
    ...parentMeta,
    { title: `${data.title} | My App` },
  ];
};
```

#### Adding Link Tags

```tsx
export const links: Route.LinksFunction = () => {
  return [
    { rel: "stylesheet", href: "/styles/product.css" },
    {
      rel: "preload",
      href: "/fonts/Inter.woff2",
      as: "font",
      type: "font/woff2",
      crossOrigin: "anonymous",
    },
    { rel: "canonical", href: "https://mystore.com/products/42" },
    { rel: "icon", href: "/favicon.svg", type: "image/svg+xml" },
  ];
};
```

#### Custom HTTP Headers

```tsx
export const headers: Route.HeadersFunction = ({
  loaderHeaders,
  parentHeaders,
  actionHeaders,
  errorHeaders,
}) => {
  return {
    "Cache-Control":
      loaderHeaders.get("Cache-Control") || "public, max-age=300",
    "X-Custom-Header": "value",
  };
};
```

### 5.11 Revalidation Control

Fine-tune when loaders re-run after navigations and actions:

```tsx
export function shouldRevalidate({
  currentUrl,
  currentParams,
  nextUrl,
  nextParams,
  formAction,
  formMethod,
  defaultShouldRevalidate,
  actionResult,
}) {
  // Skip revalidation if only the hash changed
  if (currentUrl.pathname === nextUrl.pathname &&
      currentUrl.search === nextUrl.search) {
    return false;
  }

  // Always revalidate after non-GET submissions
  if (formMethod && formMethod !== "GET") {
    return true;
  }

  // Only revalidate if specific params changed
  if (currentParams.category !== nextParams.category) {
    return true;
  }

  // Fall back to React Router's default behavior
  return defaultShouldRevalidate;
}
```

### 5.12 Polling and Real-Time Updates

#### Polling with useRevalidator

```tsx
function LiveDashboard() {
  const { data } = useLoaderData();
  const revalidator = useRevalidator();

  useEffect(() => {
    const interval = setInterval(() => {
      if (revalidator.state === "idle") {
        revalidator.revalidate();
      }
    }, 30_000); // Poll every 30 seconds

    return () => clearInterval(interval);
  }, [revalidator]);

  return <DashboardView data={data} />;
}
```

#### Revalidate on Focus

```tsx
function RevalidateOnFocus() {
  const revalidator = useRevalidator();

  useEffect(() => {
    function handleFocus() {
      if (revalidator.state === "idle") {
        revalidator.revalidate();
      }
    }

    window.addEventListener("focus", handleFocus);
    return () => window.removeEventListener("focus", handleFocus);
  }, [revalidator]);

  return null; // Render this component in root layout
}
```

### 5.13 Multi-Step Forms (Wizards)

```tsx
// Use search params to track the current step
function Wizard() {
  const [searchParams, setSearchParams] = useSearchParams();
  const step = Number(searchParams.get("step") || "1");

  return (
    <div>
      <div className="steps">
        <span className={step >= 1 ? "active" : ""}>1. Info</span>
        <span className={step >= 2 ? "active" : ""}>2. Address</span>
        <span className={step >= 3 ? "active" : ""}>3. Review</span>
      </div>

      <Form method="post">
        {step === 1 && <StepInfo />}
        {step === 2 && <StepAddress />}
        {step === 3 && <StepReview />}

        <div className="actions">
          {step > 1 && (
            <button
              type="button"
              onClick={() => setSearchParams({ step: String(step - 1) })}
            >
              Back
            </button>
          )}
          {step < 3 ? (
            <button
              type="button"
              onClick={() => setSearchParams({ step: String(step + 1) })}
            >
              Next
            </button>
          ) : (
            <button type="submit">Submit</button>
          )}
        </div>
      </Form>
    </div>
  );
}
```

### 5.14 Testing Routes

#### Testing with createRoutesStub

React Router provides `createRoutesStub` for testing routes with their loaders and actions:

```tsx
import { createRoutesStub } from "react-router";
import { render, screen, waitFor } from "@testing-library/react";
import userEvent from "@testing-library/user-event";

test("renders user profile from loader data", async () => {
  const Stub = createRoutesStub([
    {
      path: "/users/:id",
      Component: UserProfile,
      loader: () => ({
        user: { id: "1", name: "Alice", email: "[email removed]" },
      }),
    },
  ]);

  render(<Stub initialEntries={["/users/1"]} />);

  await waitFor(() => {
    expect(screen.getByText("Alice")).toBeInTheDocument();
  });
});

test("handles form submission via action", async () => {
  const actionFn = vi.fn().mockReturnValue({ success: true });

  const Stub = createRoutesStub([
    {
      path: "/contact",
      Component: ContactForm,
      action: actionFn,
    },
  ]);

  render(<Stub initialEntries={["/contact"]} />);

  await userEvent.type(screen.getByLabelText("Email"), "[email removed]");
  await userEvent.click(screen.getByText("Submit"));

  expect(actionFn).toHaveBeenCalled();
});
```

#### Testing with MemoryRouter

For simpler component tests that do not need data loading:

```tsx
import { MemoryRouter, Routes, Route } from "react-router";
import { render, screen } from "@testing-library/react";

test("renders navigation with active state", () => {
  render(
    <MemoryRouter initialEntries={["/dashboard"]}>
      <Routes>
        <Route path="/" element={<Nav />}>
          <Route path="dashboard" element={<Dashboard />} />
        </Route>
      </Routes>
    </MemoryRouter>
  );

  expect(screen.getByText("Dashboard")).toHaveClass("active");
});
```

#### Testing Hooks

```tsx
import { renderHook } from "@testing-library/react";
import { MemoryRouter } from "react-router";

test("useParams returns route parameters", () => {
  const { result } = renderHook(() => useParams(), {
    wrapper: ({ children }) => (
      <MemoryRouter initialEntries={["/users/42"]}>
        <Routes>
          <Route path="/users/:id" element={children} />
        </Routes>
      </MemoryRouter>
    ),
  });

  expect(result.current.id).toBe("42");
});
```

---

## 6. Troubleshooting

### 6.1 Common Issues

#### "No routes matched location"

**Symptom:** A blank page or console warning saying no routes matched.

**Causes and fixes:**

1. **Missing catch-all route.** Add a `path="*"` route for 404 handling:

```tsx
<Routes>
  <Route path="/" element={<Home />} />
  <Route path="*" element={<NotFound />} />
</Routes>
```

2. **Basename mismatch.** If your app is served from a subdirectory, configure `basename`:

```tsx
// Library mode
<BrowserRouter basename="/my-app">

// Framework mode (react-router.config.ts)
export default { basename: "/my-app" } satisfies Config;
```

3. **Server not configured for client-side routing.** Your web server must return `index.html` for all paths so the client-side router can handle them. For Nginx:

```nginx
location / {
  try_files $uri $uri/ /index.html;
}
```

For Apache, add a `.htaccess`:

```apache
RewriteEngine On
RewriteCond %{REQUEST_FILENAME} !-f
RewriteCond %{REQUEST_FILENAME} !-d
RewriteRule . /index.html [L]
```

#### Hydration Mismatches (SSR)

**Symptom:** React warns about hydration mismatches in the console after server-side rendering.

**Common causes:**

- Rendering browser-only values (dates, `window` properties, `Math.random()`) during SSR. These produce different output on server vs. client.
- URL imports with `?url` suffix generating different paths between server and client builds (GitHub issue #12775).
- Browser extensions injecting elements into the DOM.

**Fixes:**

- Defer browser-only rendering to `useEffect`:

```tsx
function ClientDate() {
  const [date, setDate] = useState<string>();
  useEffect(() => setDate(new Date().toLocaleDateString()), []);
  return <span>{date ?? "Loading..."}</span>;
}
```

- Use `HydrateFallback` in framework mode route modules to show a placeholder until client hydration completes.
- For `?url` imports, use standard imports or inline small assets as data URIs.

#### Stale Loader Data After Action

**Symptom:** After a form submission, the page shows outdated data.

By default, React Router revalidates all active loaders after every action. If data is still stale:

1. Ensure your action returns a value or redirects — returning `undefined` may skip revalidation.
2. Verify the loader actually re-fetches data on every call and is not served from a stale HTTP cache. Set appropriate `Cache-Control` headers.
3. If using `shouldRevalidate`, check that it returns `true` after the relevant action.
4. Force manual revalidation with `useRevalidator().revalidate()` if needed.

#### Lazy Loading Suspense Not Showing

If a `Suspense` fallback does not display during lazy route transitions (related to GitHub issue #12474):

Lazy route transitions use the `useNavigation()` state (`"loading"`) rather than React Suspense boundaries. If you want a Suspense-based loading state during initial render, use React's `lazy()` directly. For data router lazy routes, show loading UI based on `navigation.state`:

```tsx
const navigation = useNavigation();
if (navigation.state === "loading") return <Spinner />;
```

#### Duplicate Styles After Navigation

Certain versions introduced duplicated CSS when components are suspended across multiple routes (GitHub issue #14754). If you experience duplicated styles:

- Check if the issue occurs with suspended components shared across routes.
- Consider colocating styles per route module.
- Monitor the issue tracker for patches.

#### "Server-only module referenced by client"

This error occurs when a directory is named exactly `.server` and its exports are imported in client code, even if unused (GitHub issue #14997).

**Fix:** Use `.server.ts` file suffix instead of a `.server/` directory, or ensure no client-side code imports from `.server` directories.

> [!CAUTION]
> Never import from `.server` files or directories in client components, even if the import appears unused. The bundler cannot always tree-shake these imports, and they will fail at runtime in the browser.

#### Paths with Colons

React Router interprets `:` as a dynamic parameter prefix. To use a literal colon in a path (GitHub issue #11015):

```tsx
// This treats ":port" as a parameter — probably not what you want
<Route path="/proxy/:port" />

// Use a splat route and parse the path yourself
<Route path="/proxy/*" element={<Proxy />} />

// In framework mode filesystem routing, use bracket escaping
// File: routes/proxy.[colon]8080.tsx  → matches /proxy:8080
```

### 6.2 Build and Dev Server Issues

#### Vite Plugin Configuration Conflicts

If you have multiple Vite configurations or a monorepo setup that shares `flatRoutes()`, each instance can leak route state into the other (GitHub issue #15007). Keep each app's Vite config isolated and avoid sharing route resolution functions across config files.

#### pnpm Trust Policy

When using pnpm, `@react-router/dev` may require a trust policy exclusion (GitHub issue #14991). Add to your `.npmrc`:

```ini
trustPolicyExclude=@react-router/dev
```

#### TypeGen with Wildcard Folder Names

If a route file lives in a folder named `*`, the `typegen` command fails (GitHub issue #14744). Rename the folder to avoid the wildcard character.

#### Dev Server Port Conflicts

```bash
# Specify a different port
npx react-router dev --port 3001

# Use strict port (fail instead of auto-incrementing)
npx react-router dev --port 3000 --strictPort
```

### 6.3 Performance Optimization

#### Large Route Trees

For apps with hundreds of routes, enable lazy route discovery to reduce the initial manifest size:

```ts
// react-router.config.ts
export default {
  routeDiscovery: { mode: "lazy" },
} satisfies Config;
```

Only visited routes are loaded. Unvisited routes are fetched on demand when the user hovers or focuses a link.

#### Controlling Revalidation

Prevent unnecessary loader re-runs with `shouldRevalidate`:

```tsx
{
  path: "dashboard",
  loader: dashboardLoader,
  shouldRevalidate: ({ currentUrl, nextUrl, formAction, defaultShouldRevalidate }) => {
    // Only revalidate if action targeted this route or search params changed
    if (formAction?.startsWith("/dashboard")) return true;
    if (currentUrl.search !== nextUrl.search) return true;
    return false;
  },
}
```

#### Bundle Size Reduction

- **Framework mode:** Enable `v8_splitRouteModules` to split each route export (loader, component, action) into separate chunks. The client only downloads the component; loaders stay on the server.
- **Data router mode:** Use `lazy` routes to defer loading route modules until navigation.
- **Both modes:** Audit imports with your bundler's analysis tools. Avoid importing large libraries in routes that do not need them.

#### Pre-bundling Dependencies

Enable dependency pre-bundling to speed up cold starts in development:

```ts
// react-router.config.ts
export default {
  future: {
    unstable_optimizeDeps: true,
  },
} satisfies Config;
```

### 6.4 CI and Testing Stability

If using Cypress or similar E2E frameworks, some teams report increased random CI failures after upgrading (GitHub issue #12974). Mitigations:

- Increase timeouts for navigation-related assertions.
- Use `cy.intercept` to mock data-loading requests and stabilize test behavior.
- Pin to a specific React Router patch version until flaky tests are resolved.
- Ensure your test server handles concurrent requests without race conditions.

### 6.5 Middleware Redirect with useFetcher

When using the `v8_middleware` future flag, middleware that returns `redirect()` may not work correctly with `useFetcher`'s `fetcher.load()` (GitHub issue #14903). As a workaround, handle redirects in route loaders rather than middleware when the route is accessed via fetchers.

---

## 7. FAQ

### What is the difference between React Router and Remix?

Remix has been merged into React Router. Starting with React Router v7, all Remix features (SSR, loaders, actions, file-based routing, sessions, cookies) are available directly in React Router's framework mode. The `@react-router/dev` package replaces `@remix-run/dev`, and `react-router` replaces `@remix-run/react`. If you are on Remix v2, the migration path is to upgrade to React Router v7+ with framework mode.

### Should I use `react-router-dom` or `react-router`?

Import from `react-router` directly. The `react-router-dom` package is a backward-compatible re-export shim that adds no functionality. For DOM-specific exports like `RouterProvider`, import from `react-router/dom`.

### Which mode should I choose?

| Situation | Recommended Mode |
|---|---|
| New project (full-stack or SSR) | Framework mode |
| New project (client-side SPA) | Framework mode with `ssr: false` |
| Adding routing to an existing React app | Data Router (library mode) |
| Very simple routing without data loading | Declarative (library mode) |
| Migrating from React Router v5 | Declarative mode first, then upgrade to Data Router |
| Migrating from Remix v2 | Framework mode |

### Can I mix declarative and data router patterns?

No. Within a single application, use either `BrowserRouter` + `<Routes>` (declarative) **or** `createBrowserRouter` + `RouterProvider` (data router). They are separate systems that cannot be combined. Data APIs (loaders, actions, fetchers) are only available with data routers or framework mode.

### How do I handle 404 pages?

**Declarative mode:**

```tsx
<Routes>
  <Route path="/" element={<Home />} />
  {/* Catch-all — must be last */}
  <Route path="*" element={<NotFound />} />
</Routes>
```

**Data router / framework mode:** Use an `errorElement` on your root route. Any unmatched URL throws a 404 response, which is caught by the nearest error boundary:

```tsx
const router = createBrowserRouter([
  {
    path: "/",
    element: <Root />,
    errorElement: <NotFound />,
    children: [/* ... */],
  },
]);
```

In framework mode, export an `ErrorBoundary` from `app/root.tsx`.

### How do I redirect after login?

Save the intended URL in the query string or location state, then navigate to it after successful authentication. See section 5.1 for complete examples.

### How do I pass data between routes?

| Method | Use Case | Example |
|---|---|---|
| URL params (`:id`) | Resource identifiers | `/users/42` |
| Search params (`?key=val`) | Filters, pagination, sorting | `/search?q=react&page=2` |
| Location state | Transient UI state (modal backgrounds, return paths) | `navigate("/x", { state: { from: "/" } })` |
| Outlet context | Parent-to-child data | `<Outlet context={data} />` |
| `useRouteLoaderData` | Access ancestor route's loader data | `useRouteLoaderData("root")` |
| React context | Global state (auth, theme, feature flags) | Standard React context |

### Do forms work without JavaScript?

In framework mode with SSR enabled, `<Form>` works without JavaScript as a standard HTML form. The browser submits the form, the server runs the action, and returns the re-rendered page. This is progressive enhancement by default.

> [!IMPORTANT]
> Progressive enhancement with `<Form>` only works in framework mode with SSR. In SPA mode or library mode, JavaScript is required for form handling and client-side navigation.

### How do I animate route transitions?

Use the browser's View Transitions API via the `viewTransition` prop on `<Link>`, `<NavLink>`, or `navigate()`. Style transitions with CSS `::view-transition-old` and `::view-transition-new` pseudo-elements.

For broader browser support, use animation libraries like Framer Motion with `useLocation` to detect route changes.

### What React versions are supported?

React Router supports both React 18 and React 19. React 19 enables additional features like streaming SSR improvements and use of React's built-in `use()` hook for data loading patterns.

### How do I deploy a framework mode app?

1. Build your app:

```bash
npx react-router build
```

2. Run the production server with your chosen adapter:

| Adapter | Command / Setup |
|---|---|
| `@react-router/serve` | `npx react-router-serve ./build/server/index.js` |
| `@react-router/express` | Use `createRequestHandler` in your Express app |
| `@react-router/cloudflare` | Deploy with `wrangler` |
| `@react-router/architect` | Deploy with Arc |

For SPA mode (`ssr: false`), the build output in `build/client/` is static HTML/CSS/JS. Deploy it to any static hosting provider (Netlify, Vercel, S3, GitHub Pages, etc.).

### What are the CLI commands?

| Command | Description |
|---|---|
| `npx react-router build [dir]` | Build for production |
| `npx react-router dev [dir]` | Start the development server |
| `npx react-router routes [dir]` | Display the configured route tree |
| `npx react-router reveal [entry]` | Generate entry files for customization |
| `npx react-router typegen` | Generate TypeScript route types |

**Build flags:** `--mode` (`-m`), `--config` (`-c`), `--minify`, `--sourcemapClient`, `--sourcemapServer`, `--profile`, `--logLevel` (`-l`), `--clearScreen`, `--emptyOutDir`, `--assetsInlineLimit`

**Dev flags:** `--mode` (`-m`), `--config` (`-c`), `--port`, `--host`, `--open`, `--cors`, `--force`, `--strictPort`, `--profile`, `--logLevel` (`-l`), `--clearScreen`

### Where can I find architecture and deployment internals?

The Admin Guide covers architecture diagrams, full configuration reference, deployment strategies, security hardening, and internal implementation details. See the Admin Guide at [remix-run/react-router](/remix-run/react-router/admin-guide/).

---

_Generated May 2026 from remix-run/react-router at b80380d. AI-generated documentation. The library is checked by sampling (three repositories so far); this guide was not individually reviewed. Verify against the project before relying on it. Unofficial; not affiliated with or reviewed by the project._
