Skill: anderskev/react-router-v7 (version 1.1.1)

--- name: react-router-v7 description: React Router v7 best practices for data-driven routing. Use when implementing routes, loaders, actions, Form components, fetchers, navigation guards, protected routes, or URL search params. Triggers on createBrowserRouter, RouterProvider, useLoaderData, useActionData, useFetcher, NavLink, Outlet. --- # React Router v7 Best Practices ## Quick Reference **Router Setup (Data Mode)**: ```tsx import { createBrowserRouter, RouterProvider } from "react-router"; const router = createBrowserRouter([ { path: "/", Component: Root, ErrorBoundary: RootErrorBoundary, loader: rootLoader, children: [ { index: true, Component: Home }, { path: "products/:productId", Component: Product, loader: productLoader }, ], }, ]); ReactDOM.createRoot(root).render(<RouterProvider router={router} />); ``` **Framework Mode (Vite plugin)**: ```ts // routes.ts import { index, route } from "@react-router/dev/routes"; export default [ index("./home.tsx"), route("products/:pid", "./product.tsx"), ]; ``` ## Route Configuration ### Nested Routes with Outlets ```tsx createBrowserRouter([ { path: "/dashboard", Component: Dashboard, children: [ { index: true, Component: DashboardHome }, { path: "settings", Component: Settings }, ], }, ]); function Dashboard() { return ( <div> <h1>Dashboard</h1> <Outlet /> {/* Renders child routes */} </div> ); } ``` ### Dynamic Segments and Splats ```tsx { path: "teams/:teamId" } // params.teamId { path: ":lang?/categories" } // Optional segment { path: "files/*" } // Splat: params["*"] ``` ## Key Decision Points ### Form vs Fetcher **Use `<Form>`**: Creating/deleting with URL change, adding to history **Use `useFetcher`**: Inline updates, list operations, popovers - no URL change ### Loader vs useEffect **Use loader**: Data before render, server-side fetch, automatic revalidation **Use useEffect**: Client-only data, user-interaction dependent, subscriptions ## Gates (decision sequencing) Answer **in order**. **Pass** means the condition is true; pick the API on the same line and **stop**. ### `<Form>` vs `useFetcher` 1. **Must the URL or history stack change** (bookmark/share, back returns to prior screen)? - **Pass →** `<Form>` / route `action` (or `useSubmit` + navigation). **Stop.** - **Fail →** Step 2. 2. **Mutation stays on the same route** (inline edit, modal, list row, no address change)? - **Pass →** `useFetcher()`. **Stop.** - **Fail →** Re-check step 1; you may need a dedicated action route or POST to the current URL. ### `loader` vs `useEffect` 1. **Is data needed for correct first render** (or your intended `<Suspense>` boundary) for this route? - **Pass →** `loader` (Framework: `clientLoader` when appropriate). **Stop.** - **Fail →** Step 2. 2. **Fetch only after mount** from user action, timer, or subscription (not route entry)? - **Pass →** `useEffect` / event handlers. **Stop.** - **Fail →** Prefer loader + revalidation over an effect that mirrors navigation. ## Additional Documentation - **Data Loading**: See [references/loaders.md](references/loaders.md) for loader patterns, parallel loading, search params - **Mutations**: See [references/actions.md](references/actions.md) for actions, Form, fetchers, validation - **Navigation**: See [references/navigation.md](references/navigation.md) for Link, NavLink, programmatic nav - **Advanced**: See [references/advanced.md](references/advanced.md) for error boundaries, protected routes, lazy loading ## Mode Comparison | Feature | Framework Mode | Data Mode | Declarative Mode | |---------|---------------|-----------|------------------| | Setup | Vite plugin | `createBrowserRouter` | `<BrowserRouter>` | | Type Safety | Auto-generated types | Manual | Manual | | SSR Support | Built-in | Manual | Limited | | Use Case | Full-stack apps | SPAs with control | Simple/legacy |

path: ACTIONS.md
content: # Actions and Mutations

## Basic Action Pattern

```tsx
{
  path: "/projects/:id",
  action: async ({ request, params }) => {
    const formData = await request.formData();
    const title = formData.get("title");
    await updateProject(params.id, { title });
    return { success: true };
  },
  Component: Project,
}
```

## Form Submission

```tsx
function Project() {
  const actionData = useActionData();

  return (
    <Form method="post">
      <input type="text" name="title" />
      <button type="submit">Save</button>
      {actionData?.success && <p>Saved!</p>}
    </Form>
  );
}
```

## Redirect After Action

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

export async function action({ request }) {
  const formData = await request.formData();
  const project = await createProject(formData);
  return redirect(`/projects/${project.id}`);
}
```

## Form Validation

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

export async function action({ request }: Route.ActionArgs) {
  const formData = await request.formData();
  const email = String(formData.get("email"));
  const [REDACTED_SECRET]"password"));

  const errors: Record<string, string> = {};

  if (!email.includes("@")) {
    errors.email = "Invalid email address";
  }
  if (password.length < 12) {
    errors.[REDACTED_SECRET] must be at least 12 characters";
  }

  if (Object.keys(errors).length > 0) {
    return data({ errors }, { status: 400 }); // 400 prevents revalidation
  }

  return redirect("/dashboard");
}

export default function Signup() {
  const fetcher = useFetcher();
  const errors = fetcher.data?.errors;

  return (
    <fetcher.Form method="post">
      <input type="email" name="email" />
      {errors?.email && <em>{errors.email}</em>}

      <input type="password" name="password" />
      {errors?.password && <em>{errors.password}</em>}

      <button type="submit">Sign Up</button>
    </fetcher.Form>
  );
}
```

## Fetchers (Non-Navigation Mutations)

Use fetchers when you DON'T want URL changes:

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

function TodoItem({ todo }) {
  const fetcher = useFetcher();
  const isDeleting = fetcher.state !== "idle";

  return (
    <li>
      <span>{todo.title}</span>
      <fetcher.Form method="post" action="/todos/delete">
        <input type="hidden" name="id" value={todo.id} />
        <button type="submit" disabled={isDeleting}>
          {isDeleting ? "Deleting..." : "Delete"}
        </button>
      </fetcher.Form>
    </li>
  );
}
```

## Optimistic UI with Fetchers

```tsx
function Component() {
  const data = useLoaderData();
  const fetcher = useFetcher();

  // Show optimistic state while submitting
  const title = fetcher.formData?.get("title") || data.title;

  return (
    <div>
      <h1>{title}</h1>
      <fetcher.Form method="post">
        <input type="text" name="title" />
        {fetcher.state !== "idle" && <p>Saving...</p>}
      </fetcher.Form>
    </div>
  );
}
```

## Fetcher for Data Loading (Combobox)

```tsx
function UserSearchCombobox() {
  const fetcher = useFetcher<typeof loader>();

  return (
    <div>
      <fetcher.Form method="get" action="/search-users">
        <input
          type="text"
          name="q"
          onChange={(e) => fetcher.submit(e.currentTarget.form)}
        />
      </fetcher.Form>
      {fetcher.data && (
        <ul style={{ opacity: fetcher.state === "idle" ? 1 : 0.25 }}>
          {fetcher.data.map((user) => (
            <li key={user.id}>{user.name}</li>
          ))}
        </ul>
      )}
    </div>
  );
}
```

## Optimistic List Updates

```tsx
function TodoList() {
  const { todos } = useLoaderData();
  const fetcher = useFetcher();

  const displayedTodos = todos.filter(todo => {
    const isDeleting = fetcher.formData?.get("id") === todo.id;
    return !isDeleting;
  });

  return (
    <ul>
      {displayedTodos.map(todo => (
        <li key={todo.id}>
          {todo.title}
          <fetcher.Form method="post" action="/todos/delete">
            <input type="hidden" name="id" value={todo.id} />
            <button type="submit">Delete</button>
          </fetcher.Form>
        </li>
      ))}
    </ul>
  );
}
```


path: ADVANCED.md
content: # Advanced Patterns

## Error Boundaries

### Root Error Boundary (Required)

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

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

  if (isRouteErrorResponse(error)) {
    return (
      <>
        <h1>{error.status} {error.statusText}</h1>
        <p>{error.data}</p>
      </>
    );
  } else if (error instanceof Error) {
    return (
      <div>
        <h1>Error</h1>
        <p>{error.message}</p>
        <pre>{error.stack}</pre>
      </div>
    );
  } else {
    return <h1>Unknown Error</h1>;
  }
}

createBrowserRouter([
  {
    path: "/",
    ErrorBoundary: RootErrorBoundary,
    Component: Root,
  },
]);
```

### Throwing Errors in Loaders

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

export async function loader({ params }) {
  const record = await db.getRecord(params.id);
  if (!record) {
    throw data("Record Not Found", { status: 404 });
  }
  return record;
}
```

### Nested Error Boundaries

```tsx
createBrowserRouter([
  {
    path: "/app",
    ErrorBoundary: AppErrorBoundary,
    children: [
      {
        path: "invoices/:id",
        ErrorBoundary: InvoiceErrorBoundary,
        Component: Invoice,
      },
    ],
  },
]);
```

## Protected Routes

### Component-Based Protection

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

function RequireAuth({ children }: { children: JSX.Element }) {
  const auth = useAuth();
  const location = useLocation();

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

  return children;
}

// Route configuration
{
  path: "/protected",
  element: (
    <RequireAuth>
      <ProtectedPage />
    </RequireAuth>
  ),
}

// In login handler - redirect back
function LoginPage() {
  const navigate = useNavigate();
  const location = useLocation();
  const from = location.state?.from?.pathname || "/";

  function handleLogin() {
    auth.signin(() => {
      navigate(from, { replace: true });
    });
  }
}
```

### Middleware (Framework Mode)

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

async function authMiddleware({ context, request }) {
  const userId = getUserId(request);

  if (!userId) {
    throw redirect("/login");
  }

  const user = await getUserById(userId);
  context.set(userContext, user);
}

createBrowserRouter([
  {
    path: "/dashboard",
    middleware: [authMiddleware],
    Component: Dashboard,
  },
]);
```

## Lazy Loading / Code Splitting

### Data Mode Lazy Loading

```tsx
createBrowserRouter([
  {
    path: "/app",
    lazy: async () => {
      const [Component, loader] = await Promise.all([
        import("./app"),
        import("./app-loader"),
      ]);
      return { Component, loader };
    },
  },
]);
```

### Declarative Mode Lazy Loading

```tsx
import React from "react";

const About = React.lazy(() => import("./pages/About"));

<Routes>
  <Route
    path="about"
    element={
      <React.Suspense fallback={<>Loading...</>}>
        <About />
      </React.Suspense>
    }
  />
</Routes>
```

## Common Route Patterns

### Optional Segments

```tsx
{ path: ":lang?/categories" }     // Optional dynamic segment
{ path: "users/:userId/edit?" }   // Optional static segment at end
```

### Catch-All / Splat Routes

```tsx
{ path: "files/*" }

// Access splat in loader
loader: ({ params }) => {
  const filePath = params["*"]; // "path/to/file.txt"
}
```

### Multiple Params

```tsx
{ path: "users/:userId/posts/:postId" }

// params.userId, params.postId available in loader/component
```

## Index vs Path Routes

```tsx
createBrowserRouter([
  {
    path: "/dashboard",
    Component: Dashboard,
    children: [
      // Index route - renders when parent path matches exactly
      { index: true, Component: DashboardHome },

      // Path route - renders at parent + path
      { path: "settings", Component: Settings },
      { path: "profile", Component: Profile },
    ],
  },
]);
```

**Index renders at**: `/dashboard`
**Settings renders at**: `/dashboard/settings`


path: LOADERS.md
content: # Data Loading Patterns

## Basic Loader

```tsx
{
  path: "/teams/:teamId",
  loader: async ({ params, request }) => {
    const url = new URL(request.url);
    const query = url.searchParams.get("q");
    const team = await fetchTeam(params.teamId, query);
    return { team, name: team.name };
  },
  Component: Team,
}

function Team() {
  const data = useLoaderData();
  return <h1>{data.name}</h1>;
}
```

## Parallel Data Loading

Nested routes load data in parallel automatically:

```tsx
createBrowserRouter([
  {
    path: "/",
    loader: rootLoader,    // Loads in parallel
    children: [
      {
        path: "project/:id",
        loader: projectLoader, // Loads in parallel with rootLoader
      },
    ],
  },
]);
```

## Search Params in Loaders

```tsx
{
  path: "/search",
  loader: async ({ request }) => {
    const url = new URL(request.url);
    const query = url.searchParams.get("q");
    const page = url.searchParams.get("page") || "1";
    return { results: await search(query, parseInt(page)) };
  },
}

function SearchPage() {
  const { results } = useLoaderData();
  return (
    <Form method="get">
      <input type="text" name="q" />
      <button type="submit">Search</button>
    </Form>
  );
}
```

## useSearchParams Hook

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

function SearchPage() {
  const [searchParams, setSearchParams] = useSearchParams();
  const query = searchParams.get("q");

  return (
    <input
      value={query || ""}
      onChange={(e) => setSearchParams({ q: e.target.value })}
    />
  );
}
```

## Revalidation Control

```tsx
function shouldRevalidate({ currentUrl, nextUrl, formAction }) {
  return currentUrl.pathname !== nextUrl.pathname;
}

createBrowserRouter([
  {
    path: "/data",
    shouldRevalidate,
    loader: dataLoader,
  },
]);
```

## Framework Mode Loaders

```tsx
// product.tsx
import { Route } from "./+types/product";

export async function loader({ params }: Route.LoaderArgs) {
  const product = await getProduct(params.pid);
  return { product };
}

export default function Product({ loaderData }: Route.ComponentProps) {
  return <div>{loaderData.product.name}</div>;
}
```


path: NAVIGATION.md
content: # Navigation Patterns

## NavLink (Active Styling)

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

<NavLink to="/messages" end>
  Messages
</NavLink>

// CSS styling
a.active { color: red; }
a.pending { animation: pulse 1s infinite; }

// Callback styling
<NavLink
  to="/messages"
  className={({ isActive, isPending }) =>
    isActive ? "active" : isPending ? "pending" : ""
  }
>
  Messages
</NavLink>
```

## Link (No Active Styling)

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

<Link to="/login">Login</Link>
<Link to={{ pathname: "/search", search: "?q=term" }}>Search</Link>
```

## Programmatic Navigation

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

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

  // Use sparingly - only for non-user-initiated navigation
  useEffect(() => {
    if (inactivityTimeout) {
      navigate("/logout");
    }
  }, [inactivityTimeout]);

  // Or with options
  navigate("/dashboard", { replace: true });
  navigate(-1); // Go back
}
```

## Redirect in Loaders

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

export async function loader({ request }) {
  const user = await getUser(request);
  if (!user) {
    return redirect("/login");
  }
  return { user };
}
```

## Pending UI (Navigation State)

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

function Root() {
  const navigation = useNavigation();
  const isNavigating = navigation.state !== "idle";

  return (
    <div>
      {isNavigating && <GlobalSpinner />}
      <Outlet />
    </div>
  );
}
```

## Form Submission State

```tsx
function Component() {
  const navigation = useNavigation();
  const isSubmitting = navigation.formAction === "/recipes/new";

  return (
    <Form method="post" action="/recipes/new">
      <button type="submit">
        {isSubmitting ? "Saving..." : "Create Recipe"}
      </button>
    </Form>
  );
}
```

## Index Routes

```tsx
createBrowserRouter([
  {
    path: "/dashboard",
    Component: Dashboard,
    children: [
      { index: true, Component: DashboardHome }, // Renders at /dashboard
      { path: "settings", Component: Settings }, // Renders at /dashboard/settings
    ],
  },
]);
```

## Layout Routes (No Path)

```tsx
createBrowserRouter([
  {
    Component: MarketingLayout, // No path, just layout wrapper
    children: [
      { index: true, Component: Home },
      { path: "contact", Component: Contact },
    ],
  },
]);
```

## Navigate Component

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

function RequireAuth({ children }) {
  const auth = useAuth();
  const location = useLocation();

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

  return children;
}
```


path: references/actions.md
content: # Actions and Mutations

## Contents

- [Basic Action Pattern](#basic-action-pattern)
- [Form Submission](#form-submission)
- [Redirect After Action](#redirect-after-action)
- [Form Validation](#form-validation)
- [Fetchers (Non-Navigation Mutations)](#fetchers-non-navigation-mutations)
- [Optimistic UI with Fetchers](#optimistic-ui-with-fetchers)
- [Fetcher for Data Loading (Combobox)](#fetcher-for-data-loading-combobox)
- [Optimistic List Updates](#optimistic-list-updates)

---

## Basic Action Pattern

```tsx
{
  path: "/projects/:id",
  action: async ({ request, params }) => {
    const formData = await request.formData();
    const title = formData.get("title");
    await updateProject(params.id, { title });
    return { success: true };
  },
  Component: Project,
}
```

## Form Submission

```tsx
function Project() {
  const actionData = useActionData();

  return (
    <Form method="post">
      <input type="text" name="title" />
      <button type="submit">Save</button>
      {actionData?.success && <p>Saved!</p>}
    </Form>
  );
}
```

## Redirect After Action

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

export async function action({ request }) {
  const formData = await request.formData();
  const project = await createProject(formData);
  return redirect(`/projects/${project.id}`);
}
```

## Form Validation

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

export async function action({ request }: Route.ActionArgs) {
  const formData = await request.formData();
  const email = String(formData.get("email"));
  const [REDACTED_SECRET]"password"));

  const errors: Record<string, string> = {};

  if (!email.includes("@")) {
    errors.email = "Invalid email address";
  }
  if (password.length < 12) {
    errors.[REDACTED_SECRET] must be at least 12 characters";
  }

  if (Object.keys(errors).length > 0) {
    return data({ errors }, { status: 400 }); // 400 prevents revalidation
  }

  return redirect("/dashboard");
}

export default function Signup() {
  const fetcher = useFetcher();
  const errors = fetcher.data?.errors;

  return (
    <fetcher.Form method="post">
      <input type="email" name="email" />
      {errors?.email && <em>{errors.email}</em>}

      <input type="password" name="password" />
      {errors?.password && <em>{errors.password}</em>}

      <button type="submit">Sign Up</button>
    </fetcher.Form>
  );
}
```

## Fetchers (Non-Navigation Mutations)

Use fetchers when you DON'T want URL changes:

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

function TodoItem({ todo }) {
  const fetcher = useFetcher();
  const isDeleting = fetcher.state !== "idle";

  return (
    <li>
      <span>{todo.title}</span>
      <fetcher.Form method="post" action="/todos/delete">
        <input type="hidden" name="id" value={todo.id} />
        <button type="submit" disabled={isDeleting}>
          {isDeleting ? "Deleting..." : "Delete"}
        </button>
      </fetcher.Form>
    </li>
  );
}
```

## Optimistic UI with Fetchers

```tsx
function Component() {
  const data = useLoaderData();
  const fetcher = useFetcher();

  // Show optimistic state while submitting
  const title = fetcher.formData?.get("title") || data.title;

  return (
    <div>
      <h1>{title}</h1>
      <fetcher.Form method="post">
        <input type="text" name="title" />
        {fetcher.state !== "idle" && <p>Saving...</p>}
      </fetcher.Form>
    </div>
  );
}
```

## Fetcher for Data Loading (Combobox)

```tsx
function UserSearchCombobox() {
  const fetcher = useFetcher<typeof loader>();

  return (
    <div>
      <fetcher.Form method="get" action="/search-users">
        <input
          type="text"
          name="q"
          onChange={(e) => fetcher.submit(e.currentTarget.form)}
        />
      </fetcher.Form>
      {fetcher.data && (
        <ul style={{ opacity: fetcher.state === "idle" ? 1 : 0.25 }}>
          {fetcher.data.map((user) => (
            <li key={user.id}>{user.name}</li>
          ))}
        </ul>
      )}
    </div>
  );
}
```

## Optimistic List Updates

```tsx
function TodoList() {
  const { todos } = useLoaderData();
  const fetcher = useFetcher();

  const displayedTodos = todos.filter(todo => {
    const isDeleting = fetcher.formData?.get("id") === todo.id;
    return !isDeleting;
  });

  return (
    <ul>
      {displayedTodos.map(todo => (
        <li key={todo.id}>
          {todo.title}
          <fetcher.Form method="post" action="/todos/delete">
            <input type="hidden" name="id" value={todo.id} />
            <button type="submit">Delete</button>
          </fetcher.Form>
        </li>
      ))}
    </ul>
  );
}
```


path: references/advanced.md
content: # Advanced Patterns

## Contents

- [Error Boundaries](#error-boundaries)
  - [Root Error Boundary (Required)](#root-error-boundary-required)
  - [Throwing Errors in Loaders](#throwing-errors-in-loaders)
  - [Nested Error Boundaries](#nested-error-boundaries)
- [Protected Routes](#protected-routes)
  - [Component-Based Protection](#component-based-protection)
  - [Middleware (Framework Mode)](#middleware-framework-mode)
- [Lazy Loading / Code Splitting](#lazy-loading--code-splitting)
  - [Data Mode Lazy Loading](#data-mode-lazy-loading)
  - [Declarative Mode Lazy Loading](#declarative-mode-lazy-loading)
- [Common Route Patterns](#common-route-patterns)
  - [Optional Segments](#optional-segments)
  - [Catch-All / Splat Routes](#catch-all--splat-routes)
  - [Multiple Params](#multiple-params)
- [Index vs Path Routes](#index-vs-path-routes)

---

## Error Boundaries

### Root Error Boundary (Required)

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

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

  if (isRouteErrorResponse(error)) {
    return (
      <>
        <h1>{error.status} {error.statusText}</h1>
        <p>{error.data}</p>
      </>
    );
  } else if (error instanceof Error) {
    return (
      <div>
        <h1>Error</h1>
        <p>{error.message}</p>
        <pre>{error.stack}</pre>
      </div>
    );
  } else {
    return <h1>Unknown Error</h1>;
  }
}

createBrowserRouter([
  {
    path: "/",
    ErrorBoundary: RootErrorBoundary,
    Component: Root,
  },
]);
```

### Throwing Errors in Loaders

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

export async function loader({ params }) {
  const record = await db.getRecord(params.id);
  if (!record) {
    throw data("Record Not Found", { status: 404 });
  }
  return record;
}
```

### Nested Error Boundaries

```tsx
createBrowserRouter([
  {
    path: "/app",
    ErrorBoundary: AppErrorBoundary,
    children: [
      {
        path: "invoices/:id",
        ErrorBoundary: InvoiceErrorBoundary,
        Component: Invoice,
      },
    ],
  },
]);
```

## Protected Routes

### Component-Based Protection

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

function RequireAuth({ children }: { children: JSX.Element }) {
  const auth = useAuth();
  const location = useLocation();

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

  return children;
}

// Route configuration
{
  path: "/protected",
  element: (
    <RequireAuth>
      <ProtectedPage />
    </RequireAuth>
  ),
}

// In login handler - redirect back
function LoginPage() {
  const navigate = useNavigate();
  const location = useLocation();
  const from = location.state?.from?.pathname || "/";

  function handleLogin() {
    auth.signin(() => {
      navigate(from, { replace: true });
    });
  }
}
```

### Middleware (Framework Mode Only)

Middleware requires Framework Mode and the `future.v8_middleware` flag. Export middleware from route modules:

```tsx
// app/routes/dashboard.tsx (Framework Mode)
import { redirect, createContext } from "react-router";

export const userContext = createContext<User | null>(null);

export const middleware = [
  async function authMiddleware({ request, context }, next) {
    const userId = getUserId(request);

    if (!userId) {
      throw redirect("/login");
    }

    const user = await getUserById(userId);
    context.set(userContext, user);

    return next();
  },
];

export async function loader({ context }: Route.LoaderArgs) {
  const user = context.get(userContext);
  return { user };
}
```

Note: Middleware is NOT available in Data Mode (createBrowserRouter). Use loaders for auth checks in Data Mode.

## Lazy Loading / Code Splitting

### Data Mode Lazy Loading

```tsx
createBrowserRouter([
  {
    path: "/app",
    lazy: async () => {
      const [Component, loader] = await Promise.all([
        import("./app"),
        import("./app-loader"),
      ]);
      return { Component, loader };
    },
  },
]);
```

### Declarative Mode Lazy Loading

```tsx
import React from "react";

const About = React.lazy(() => import("./pages/About"));

<Routes>
  <Route
    path="about"
    element={
      <React.Suspense fallback={<>Loading...</>}>
        <About />
      </React.Suspense>
    }
  />
</Routes>
```

## Common Route Patterns

### Optional Segments

```tsx
{ path: ":lang?/categories" }     // Optional dynamic segment
{ path: "users/:userId/edit?" }   // Optional static segment at end
```

### Catch-All / Splat Routes

```tsx
{ path: "files/*" }

// Access splat in loader
loader: ({ params }) => {
  const filePath = params["*"]; // "path/to/file.txt"
}
```

### Multiple Params

```tsx
{ path: "users/:userId/posts/:postId" }

// params.userId, params.postId available in loader/component
```

## Index vs Path Routes

```tsx
createBrowserRouter([
  {
    path: "/dashboard",
    Component: Dashboard,
    children: [
      // Index route - renders when parent path matches exactly
      { index: true, Component: DashboardHome },

      // Path route - renders at parent + path
      { path: "settings", Component: Settings },
      { path: "profile", Component: Profile },
    ],
  },
]);
```

**Index renders at**: `/dashboard`
**Settings renders at**: `/dashboard/settings`


path: references/loaders.md
content: # Data Loading Patterns

## Contents

- [Basic Loader](#basic-loader)
- [Parallel Data Loading](#parallel-data-loading)
- [Search Params in Loaders](#search-params-in-loaders)
- [useSearchParams Hook](#usesearchparams-hook)
- [Revalidation Control](#revalidation-control)
- [Framework Mode Loaders](#framework-mode-loaders)

---

## Basic Loader

```tsx
{
  path: "/teams/:teamId",
  loader: async ({ params, request }) => {
    const url = new URL(request.url);
    const query = url.searchParams.get("q");
    const team = await fetchTeam(params.teamId, query);
    return { team, name: team.name };
  },
  Component: Team,
}

function Team() {
  const data = useLoaderData();
  return <h1>{data.name}</h1>;
}
```

## Parallel Data Loading

Nested routes load data in parallel automatically:

```tsx
createBrowserRouter([
  {
    path: "/",
    loader: rootLoader,    // Loads in parallel
    children: [
      {
        path: "project/:id",
        loader: projectLoader, // Loads in parallel with rootLoader
      },
    ],
  },
]);
```

## Search Params in Loaders

```tsx
{
  path: "/search",
  loader: async ({ request }) => {
    const url = new URL(request.url);
    const query = url.searchParams.get("q");
    const page = url.searchParams.get("page") || "1";
    return { results: await search(query, parseInt(page)) };
  },
}

function SearchPage() {
  const { results } = useLoaderData();
  return (
    <Form method="get">
      <input type="text" name="q" />
      <button type="submit">Search</button>
    </Form>
  );
}
```

## useSearchParams Hook

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

function SearchPage() {
  const [searchParams, setSearchParams] = useSearchParams();
  const query = searchParams.get("q");

  return (
    <input
      value={query || ""}
      onChange={(e) => setSearchParams({ q: e.target.value })}
    />
  );
}
```

## Revalidation Control

```tsx
function shouldRevalidate({ currentUrl, nextUrl, formAction }) {
  return currentUrl.pathname !== nextUrl.pathname;
}

createBrowserRouter([
  {
    path: "/data",
    shouldRevalidate,
    loader: dataLoader,
  },
]);
```

## Framework Mode Loaders

```tsx
// product.tsx
import { Route } from "./+types/product";

export async function loader({ params }: Route.LoaderArgs) {
  const product = await getProduct(params.pid);
  return { product };
}

export default function Product({ loaderData }: Route.ComponentProps) {
  return <div>{loaderData.product.name}</div>;
}
```


path: references/navigation.md
content: # Navigation Patterns

## Contents

- [NavLink (Active Styling)](#navlink-active-styling)
- [Link (No Active Styling)](#link-no-active-styling)
- [Programmatic Navigation](#programmatic-navigation)
- [Redirect in Loaders](#redirect-in-loaders)
- [Pending UI (Navigation State)](#pending-ui-navigation-state)
- [Form Submission State](#form-submission-state)
- [Index Routes](#index-routes)
- [Layout Routes (No Path)](#layout-routes-no-path)
- [Navigate Component](#navigate-component)

---

## NavLink (Active Styling)

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

<NavLink to="/messages" end>
  Messages
</NavLink>

// CSS styling
a.active { color: red; }
a.pending { animation: pulse 1s infinite; }

// Callback styling
<NavLink
  to="/messages"
  className={({ isActive, isPending }) =>
    isActive ? "active" : isPending ? "pending" : ""
  }
>
  Messages
</NavLink>
```

## Link (No Active Styling)

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

<Link to="/login">Login</Link>
<Link to={{ pathname: "/search", search: "?q=term" }}>Search</Link>
```

## Programmatic Navigation

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

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

  // Use sparingly - only for non-user-initiated navigation
  useEffect(() => {
    if (inactivityTimeout) {
      navigate("/logout");
    }
  }, [inactivityTimeout]);

  // Or with options
  navigate("/dashboard", { replace: true });
  navigate(-1); // Go back
}
```

## Redirect in Loaders

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

export async function loader({ request }) {
  const user = await getUser(request);
  if (!user) {
    return redirect("/login");
  }
  return { user };
}
```

## Pending UI (Navigation State)

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

function Root() {
  const navigation = useNavigation();
  const isNavigating = navigation.state !== "idle";

  return (
    <div>
      {isNavigating && <GlobalSpinner />}
      <Outlet />
    </div>
  );
}
```

## Form Submission State

```tsx
function Component() {
  const navigation = useNavigation();
  const isSubmitting = navigation.formAction === "/recipes/new";

  return (
    <Form method="post" action="/recipes/new">
      <button type="submit">
        {isSubmitting ? "Saving..." : "Create Recipe"}
      </button>
    </Form>
  );
}
```

## Index Routes

```tsx
createBrowserRouter([
  {
    path: "/dashboard",
    Component: Dashboard,
    children: [
      { index: true, Component: DashboardHome }, // Renders at /dashboard
      { path: "settings", Component: Settings }, // Renders at /dashboard/settings
    ],
  },
]);
```

## Layout Routes (No Path)

```tsx
createBrowserRouter([
  {
    Component: MarketingLayout, // No path, just layout wrapper
    children: [
      { index: true, Component: Home },
      { path: "contact", Component: Contact },
    ],
  },
]);
```

## Navigate Component

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

function RequireAuth({ children }) {
  const auth = useAuth();
  const location = useLocation();

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

  return children;
}
```


Security scan verdicts — ClawScan: clean | static analysis: clean | VirusTotal: clean | SkillSpector: clean (LOW)