git:20260320.766fb9e to git:20260906.ae0cc61

199 added, 242 removed. Audit A to A.

---
name: web-meta-framework-remix
description: File-based routing, loaders, actions, defer streaming, useFetcher, error boundaries, progressive enhancement
---
# Remix / React Router v7 Framework Patterns
- > **Quick Guide:** Each route exports a `loader` for reads and an `action` for writes. Both run on the server. Data flows through loaders, mutations go through actions, forms work without JavaScript, and nested routes enable parallel data loading. `json()` and `defer()` are deprecated in React Router v7 -- return raw objects instead, use `data()` for custom headers/status.
+ > **Quick Guide:** A route exports a `loader` for reads and an `action` for writes, both server-only,
+ > so a component never fetches its own data. Forms submit without JavaScript and nested routes load
+ > in parallel. The version fact that changes every example below: React Router v7 deprecates `json()`
+ > and `defer()` — return raw objects and raw Promises, and use `data()` only when you need a custom
+ > status or header.
+ **Detailed Resources:**
+
+ - [examples/core.md](examples/core.md) — the routes directory, file naming, a route end to end
+ - [examples/loaders.md](examples/loaders.md) — auth, pagination, search params, caching headers
+ - [examples/actions.md](examples/actions.md) — validation, accessible error display, delete with confirmation
+ - [examples/forms.md](examples/forms.md) — several forms in one route through the `intent` field
+ - [examples/nested-routes.md](examples/nested-routes.md) — layouts, index routes, pathless layouts
+ - [examples/error-handling.md](examples/error-handling.md) — error boundaries that branch on status
+ - [examples/optimistic.md](examples/optimistic.md) — optimistic UI and debounced search with `useFetcher`
+ - [examples/deferred.md](examples/deferred.md) — streaming with `Suspense` and `Await`
+ - [examples/resource-routes.md](examples/resource-routes.md) — JSON APIs, webhooks, file downloads
+ - [examples/meta.md](examples/meta.md) — titles, Open Graph, Twitter cards, canonical URLs
+ - [examples/react-router-v7.md](examples/react-router-v7.md) — `routes.ts`, generated types, `clientAction`, Single Fetch
+ - [reference.md](reference.md) — decision trees, route module exports, hooks, response utilities
+
---
- <migration_notice>
+ ## Which path applies
- ## IMPORTANT: React Router v7 Migration
+ - **On Remix v2** — the examples in this skill are written for it: `@remix-run/*` imports,
+ `json()`, `defer()` and `useLoaderData<typeof loader>()`.
+ - **On React Router v7 framework mode** — the concepts carry over unchanged and the API does not.
+ Read [examples/react-router-v7.md](examples/react-router-v7.md) first and translate as you go; the
+ mapping table is below.
+ - **Building a page** — a `loader`, a default export, and an `ErrorBoundary`; follow
+ [examples/core.md](examples/core.md).
+ - **Building an endpoint** — omit the default export and the route becomes a resource route; follow
+ [examples/resource-routes.md](examples/resource-routes.md).
- **Remix has merged into React Router v7.** What was planned as Remix v3 is now React Router v7 "framework mode".
+ ---
- | Remix v2 (Deprecated) | React Router v7 (Current) |
- | --------------------------------- | ------------------------------------------------ |
- | `json(data)` | Return raw objects directly |
- | `json(data, { status, headers })` | `data(data, { status, headers })` |
- | `defer({ key: promise })` | Return `{ key: promise }` with Single Fetch |
- | `@remix-run/node` imports | `react-router` / `@react-router/node` |
- | `LoaderFunctionArgs` | `Route.LoaderArgs` (generated types) |
- | `ActionFunctionArgs` | `Route.ActionArgs` (generated types) |
- | `useLoaderData<typeof loader>()` | `loaderData` prop via `Route.ComponentProps` |
- | `RemixServer` | `ServerRouter` (from `react-router`) |
- | `RemixBrowser` | `HydratedRouter` (from `react-router/dom`) |
- | File-based routing (automatic) | `routes.ts` + optional `@react-router/fs-routes` |
+ <migration_notice>
- **This skill covers both Remix v2 and React Router v7 patterns.** Examples use Remix v2 imports by default with RR v7 equivalents documented in [examples/react-router-v7.md](examples/react-router-v7.md).
+ ## Remix v2 to React Router v7
- **Migration guide:** [Upgrading from Remix](https://reactrouter.com/upgrading/remix)
+ Remix has merged into React Router v7. What was planned as Remix v3 is React Router v7's "framework
+ mode".
+ | Remix v2 | React Router v7 |
+ | --------------------------------- | ---------------------------------------------------- |
+ | `json(data)` | Return the raw object |
+ | `json(data, { status, headers })` | `data(data, { status, headers })` |
+ | `defer({ key: promise })` | Return `{ key: promise }` — Single Fetch streams it |
+ | `@remix-run/node` imports | `react-router` / `@react-router/node` |
+ | `LoaderFunctionArgs` | `Route.LoaderArgs` (generated) |
+ | `ActionFunctionArgs` | `Route.ActionArgs` (generated) |
+ | `useLoaderData<typeof loader>()` | `loaderData` from `Route.ComponentProps` |
+ | `RemixServer` | `ServerRouter` |
+ | `RemixBrowser` | `HydratedRouter` (from `react-router/dom`) |
+ | File-based routing by default | `routes.ts`, with `@react-router/fs-routes` optional |
+
+ Migration guide: [Upgrading from Remix](https://reactrouter.com/upgrading/remix)
+
</migration_notice>
---
<critical_requirements>
- ## CRITICAL: Before Using This Skill
-
- > **All code must follow project conventions in CLAUDE.md** (kebab-case, named exports, import ordering, `import type`, named constants)
+ ## Before writing Remix code
- **(You MUST export loaders and actions as named exports from route modules only -- they do not work in non-route files)**
+ **Export `loader` and `action` from route modules only.** The build wires them up by route, so the
+ same export in a helper file is dead code that silently never runs.
- **(You MUST throw Response objects for expected errors (404, 403) -- use ErrorBoundary for handling)**
+ **Throw a Response for an expected failure — 404, 403 — and let the `ErrorBoundary` render it.**
+ Returning `null` instead pushes a null check onto every consumer and loses the status code.
- **(You MUST await critical data and return non-critical data as Promises for streaming)**
+ **Await the data the page cannot render without, and return the rest as Promises.** Anything awaited
+ delays the first byte; anything returned as a Promise streams in behind it.
- **(You MUST use named constants for HTTP status codes -- no magic numbers)**
+ **Name HTTP status codes as constants.** `HTTP_NOT_FOUND` says what the branch is for, where `404`
+ has to be recognised.
</critical_requirements>
---
- **Auto-detection:** Remix routes, React Router v7, loader function, action function, clientAction, clientLoader, useLoaderData, useActionData, useFetcher, defer, ErrorBoundary, Form component, meta function, links function, Single Fetch, ServerRouter, HydratedRouter, Route.LoaderArgs, Route.ComponentProps, shouldRevalidate
-
- **When to use:**
-
- - Building full-stack React applications with server-side rendering
- - Implementing data loading with loaders and mutations with actions
- - Creating progressively enhanced forms that work without JavaScript
- - Streaming non-critical data with defer/Promises and Suspense
- - Handling errors gracefully with route-level ErrorBoundary
+ **Auto-detection:** loader, action, clientLoader, clientAction, useLoaderData, useActionData,
+ useFetcher, useNavigation, useRouteError, isRouteErrorResponse, ErrorBoundary, HydrateFallback,
+ shouldRevalidate, defer, Await, Outlet, NavLink, meta function, links function, @remix-run/node,
+ @remix-run/react, react-router, ServerRouter, HydratedRouter, Route.LoaderArgs, Route.ComponentProps,
+ routes.ts, Single Fetch, app/routes/
- **When NOT to use:**
+ **Applies to:**
- - Static sites without server-side logic
- - Simple SPAs without server rendering needs
- - Projects already committed to a different meta-framework
+ - File-based routing, including nested layouts, index routes and pathless layouts
+ - Server-side data loading in loaders, and mutations in actions
+ - Forms that work before hydration and better after it
+ - Streaming non-critical data behind `Suspense` and `Await`
+ - Non-navigating mutations, optimistic UI and debounced search with `useFetcher`
+ - Route-scoped error boundaries that branch on HTTP status
+ - SEO through `meta`, and stylesheets and preloads through `links`
+ - Resource routes: JSON APIs, webhooks, file downloads
- **Key patterns covered:**
+ **Handled elsewhere:**
- - File-based routing (routes/, \_index, $params, \_layout)
- - Loaders for server-side data fetching
- - Actions for mutations with progressive enhancement
- - Streaming with defer() / raw Promises (RR v7)
- - useFetcher for non-navigation mutations and optimistic UI
- - Error boundaries with multi-status handling
- - Meta and Links functions for SEO
- - Resource routes (API endpoints, file downloads)
- - Nested routing with parallel data loading
- - React Router v7 migration (Single Fetch, type generation, clientAction)
+ - Persistence — a loader queries and an action writes; neither the client nor the query shape is
+ settled here
+ - Session and password handling — this skill's examples call an auth layer and read what it returns
+ - Schema validation — an action parses `FormData`; which library defines the schema is a separate
+ choice
+ - Styling — `links` returns stylesheet descriptors, and what is in them is someone else's concern
+ - React component authoring itself, outside the route module's own exports
---
<philosophy>
## Philosophy
- Remix simplifies full-stack development to a single mental model: **each route exports a loader for reads and an action for writes**. Both functions execute exclusively on the server, enabling direct database access without exposing secrets to the client.
-
- **Core Principles:**
+ Remix collapses full-stack development to one mental model: **a route exports a loader for reads and
+ an action for writes, and both run only on the server**. That is what lets a route query a database
+ directly without a secret reaching the browser, and it is why there is no client-side fetching
+ library in the picture.
- 1. **Server-first data loading**: Loaders run on the server before rendering, eliminating client-side data fetching waterfalls
- 2. **Progressive enhancement**: Forms work with plain POST requests -- JavaScript enhances but isn't required
- 3. **HTTP semantics**: Caching uses standard HTTP headers (Cache-Control), not framework-specific solutions
- 4. **Nested routes**: URL segments map to component hierarchy, enabling parallel data loading
- 5. **Web standards**: Uses Fetch API Request/Response objects throughout
+ Four things follow:
- **Data Flow:**
+ 1. **No fetch waterfalls** — loaders run before the component renders, and nested loaders run in
+ parallel with each other rather than in sequence down the tree
+ 2. **Progressive enhancement is the default** — a `<Form>` is a real form; JavaScript makes the
+ submission smoother rather than making it possible
+ 3. **HTTP semantics rather than framework ones** — caching is `Cache-Control`, errors are status
+ codes, and requests and responses are the platform's own objects
+ 4. **The URL is the state** — a nested URL maps to a nested component tree, and search params are
+ where filter and pagination state lives
```
- URL Change -> Loader(s) Execute -> Component Renders -> User Interacts
- |
- Action Executes -> Loaders Revalidate
+ URL change -> loaders run in parallel -> component renders -> user submits
+ |
+ action runs -> loaders revalidate
```
</philosophy>
---
<patterns>
- ## Core Patterns
+ ## Core patterns
### Pattern 1: File-Based Routing
- Files in `app/routes/` become URL paths. File naming conventions control nesting, layouts, and dynamic segments.
+ Files in `app/routes/` become URLs; the naming characters control nesting and dynamic segments.
- | File Name | URL | Description |
- | ----------------- | ------------- | ----------------------------- |
- | `_index.tsx` | `/` | Index route (root) |
- | `about.tsx` | `/about` | Static route |
- | `blog.$slug.tsx` | `/blog/:slug` | Dynamic parameter |
- | `blog_.tsx` | `/blog` | Pathless layout escape |
- | `_auth.tsx` | (none) | Layout route (no URL segment) |
- | `_auth.login.tsx` | `/login` | Route nested in layout |
- | `$.tsx` | `/*` | Splat/catch-all route |
+ | File | URL | What the name does |
+ | ----------------- | ------------- | -------------------------------------- |
+ | `_index.tsx` | `/` | Index route |
+ | `about.tsx` | `/about` | Static segment |
+ | `blog.$slug.tsx` | `/blog/:slug` | `$` marks a dynamic parameter |
+ | `blog_.tsx` | `/blog` | Trailing `_` escapes the parent layout |
+ | `_auth.tsx` | none | Leading `_` makes a pathless layout |
+ | `_auth.login.tsx` | `/login` | Nested inside that layout |
+ | `$.tsx` | `/*` | Splat / catch-all |
```typescript
- // app/routes/blog.$slug.tsx -- dynamic route with loader
- import type { LoaderFunctionArgs } from "@remix-run/node";
- import { useLoaderData } from "@remix-run/react";
-
+ // app/routes/blog.$slug.tsx
const HTTP_NOT_FOUND = 404;
export async function loader({ params }: LoaderFunctionArgs) {
- const post = await db.post.findUnique({ where: { slug: params.slug } });
+ const post = await getPostBySlug(params.slug);
if (!post) throw new Response("Not Found", { status: HTTP_NOT_FOUND });
return { post };
}
```
- **Why good:** File names map directly to URLs, `$` prefix for dynamic segments, loader params are typed, named constant for status code
-
- See [examples/core.md](examples/core.md) for complete route examples and [examples/nested-routes.md](examples/nested-routes.md) for layout nesting patterns.
-
- ---
+ Full code: [examples/core.md](examples/core.md) and
+ [examples/nested-routes.md](examples/nested-routes.md)
- ### Pattern 2: Loaders for Data Fetching
+ ### Pattern 2: Loaders
- Loaders are server-only functions that provide data to routes. They run on initial server render and on client navigation via fetch.
+ A loader runs on the server for the initial render and over fetch on every client navigation
+ afterwards.
```typescript
- const HTTP_NOT_FOUND = 404;
-
export async function loader({ params, request }: LoaderFunctionArgs) {
- const user = await db.user.findUnique({ where: { id: params.userId } });
+ const user = await getUser(params.userId);
if (!user) {
throw json({ message: "User not found" }, { status: HTTP_NOT_FOUND });
}
return json({ user });
}
```
- **Key rules:**
-
- - Always throw Response for expected errors (triggers ErrorBoundary)
- - Use `useLoaderData<typeof loader>()` for type-safe access (or `Route.ComponentProps` in RR v7)
- - Loaders run on every navigation -- parent loaders re-run even for child route changes
- - Use `shouldRevalidate` to optimize unnecessary re-runs
-
- See [examples/loaders.md](examples/loaders.md) for authentication, pagination, and caching examples.
+ Parent loaders re-run when a child route changes — `shouldRevalidate` is the opt-out.
- ---
+ Full code: [examples/loaders.md](examples/loaders.md)
- ### Pattern 3: Actions for Mutations
+ ### Pattern 3: Actions
- Actions handle non-GET requests (POST, PUT, DELETE, PATCH). They run before loaders and enable progressive form handling.
+ An action handles every non-GET method, runs before the loaders, and the loaders revalidate after it.
```typescript
export async function action({ request }: ActionFunctionArgs) {
const formData = await request.formData();
- const intent = formData.get("intent");
- switch (intent) {
- case "update": {
- /* ... */ return json({ success: true });
- }
- case "delete": {
- /* ... */ return redirect("/items");
- }
+ switch (formData.get("intent")) {
+ case "update":
+ return json({ success: true });
+ case "delete":
+ return redirect("/items");
default:
- throw new Error(`Unknown intent: ${intent}`);
+ throw new Error(`Unknown intent`);
}
}
```
- **Key rules:**
-
- - Use hidden `intent` field for multiple actions in one route
- - Redirect after successful mutations to prevent double-submission
- - Return validation errors with `json({ errors }, { status: 400 })`
- - Forms work without JavaScript -- progressive enhancement by default
-
- See [examples/actions.md](examples/actions.md) for validation and [examples/forms.md](examples/forms.md) for multi-form patterns.
+ A hidden `intent` field is how one route serves several forms. Redirect after a successful mutation
+ so a refresh does not resubmit.
- ---
+ Full code: [examples/actions.md](examples/actions.md) and [examples/forms.md](examples/forms.md)
- ### Pattern 4: Streaming with defer / Promises
+ ### Pattern 4: Streaming
- Await critical data, return Promises for non-critical data that can stream in.
+ Await what the page needs; hand back the rest as Promises.
```typescript
- // Remix v2: use defer()
- return defer({
- user, // Awaited -- critical
- analytics: getAnalytics(), // Promise -- streams in
- });
+ // Remix v2
+ return defer({ user, analytics: getAnalytics() });
- // React Router v7: return raw objects with Promises
- return {
- user, // Awaited -- critical
- analytics: getAnalytics(), // Promise -- streams via Single Fetch
- };
+ // React Router v7 — Single Fetch streams a raw Promise
+ return { user, analytics: getAnalytics() };
```
- Render streamed data with `<Suspense>` + `<Await>`:
-
```tsx
<Suspense fallback={<Skeleton />}>
<Await resolve={analytics} errorElement={<p>Failed to load</p>}>
{(data) => <Chart data={data} />}
</Await>
</Suspense>
```
- **When to stream:** Analytics, comments, recommendations, secondary content below the fold.
- **When NOT to stream:** Auth state, page title, SEO-critical content, data for page structure.
-
- See [examples/deferred.md](examples/deferred.md) for complete streaming examples.
+ Stream analytics, comments and recommendations. Await auth state, the page title and anything a
+ crawler reads.
- ---
+ Full code: [examples/deferred.md](examples/deferred.md)
- ### Pattern 5: useFetcher for Non-Navigation Mutations
+ ### Pattern 5: useFetcher
- `useFetcher` enables data loading and mutations without page navigation. Essential for inline interactions.
+ A fetcher submits or loads without navigating, which is what inline interactions need.
```typescript
const fetcher = useFetcher();
- // Optimistic UI: show expected state immediately
+ // Optimistic UI: the in-flight submission is already in fetcher.formData
const optimisticIsLiked = fetcher.formData
? fetcher.formData.get("liked") === "true"
: isLiked;
```
- **Use `<Form>` for:** Create/login/wizards -- actions that should change the URL.
- **Use `useFetcher` for:** Like buttons, toggles, inline editing, search autocomplete.
-
- See [examples/optimistic.md](examples/optimistic.md) for optimistic UI and debounced search.
+ `<Form>` for anything that should change the URL; `useFetcher` for likes, toggles, inline edits and
+ autocomplete.
- ---
+ Full code: [examples/optimistic.md](examples/optimistic.md)
### Pattern 6: Error Boundaries
- Export `ErrorBoundary` from route modules. Distinguish between thrown Response errors and unexpected JavaScript errors.
+ An exported `ErrorBoundary` catches everything below it in the route tree, and the first branch tells
+ a thrown Response from an unexpected exception.
- ```typescript
+ ```tsx
export function ErrorBoundary() {
const error = useRouteError();
if (isRouteErrorResponse(error)) {
- // Thrown Response: render status-specific UI
- return <div role="alert"><h1>{error.status}</h1></div>;
- }
-
- if (error instanceof Error) {
- // Unexpected error: generic fallback
- return <div role="alert"><h1>Unexpected Error</h1></div>;
+ return (
+ <div role="alert">
+ <h1>{error.status}</h1>
+ </div>
+ );
}
- return <div role="alert"><h1>Unknown Error</h1></div>;
+ return (
+ <div role="alert">
+ <h1>Unexpected Error</h1>
+ </div>
+ );
}
```
- **Key rules:**
-
- - Throw `json({ message }, { status: 404 })` for expected errors
- - ErrorBoundary is route-scoped -- rest of the page stays functional
- - Use named constants for HTTP status codes
- - `isRouteErrorResponse()` checks if error was a thrown Response
-
- See [examples/error-handling.md](examples/error-handling.md) for multi-status error boundaries.
+ The boundary is route-scoped, so the rest of the page keeps working.
- ---
+ Full code: [examples/error-handling.md](examples/error-handling.md)
- ### Pattern 7: Meta and Links Functions
+ ### Pattern 7: Meta and Links
- Export `meta` for SEO metadata and `links` for stylesheets/preloads.
+ `meta` builds the head tags from the loader's data; `links` declares stylesheets and preloads.
```typescript
export const meta: MetaFunction<typeof loader> = ({ data }) => {
if (!data) return [{ title: "Not Found" }];
return [
{ title: `${data.post.title} | ${SITE_NAME}` },
{ property: "og:title", content: data.post.title },
{ tagName: "link", rel: "canonical", href: url },
];
};
```
- **Gotcha:** `meta` function receives null data on error -- always handle the missing data case.
- **Gotcha:** `links` function cannot access loader data -- use `meta` with `tagName: "link"` for dynamic links.
-
- See [examples/meta.md](examples/meta.md) for Open Graph and Twitter Card patterns.
+ `meta` receives `undefined` data when the loader threw, and `links` cannot see loader data at all —
+ a dynamic `<link>` goes through `meta` with `tagName: "link"`.
- ---
+ Full code: [examples/meta.md](examples/meta.md)
### Pattern 8: Resource Routes
- Routes without a default export become resource routes -- useful for APIs, webhooks, and file downloads.
+ A route module with no default export renders nothing and returns whatever its loader or action
+ does.
```typescript
- // app/routes/api.health.ts (no default export = resource route)
+ // app/routes/api.health.ts
export async function loader() {
return json({ status: "healthy", timestamp: new Date().toISOString() });
}
```
- See [examples/resource-routes.md](examples/resource-routes.md) for webhook and file download examples.
-
- ---
+ Full code: [examples/resource-routes.md](examples/resource-routes.md)
- ### Pattern 9: Nested Routes and Layouts
+ ### Pattern 9: Nested Routes
- Nested routes share parent layouts and load data in parallel. Parent loaders provide shared data, child loaders run concurrently.
+ Nested routes share the parent's layout and load alongside it rather than after it, so an auth check
+ in a parent layout protects every child.
- | Pattern | Purpose |
- | --------------------- | ---------------------------------------------- |
- | `admin.tsx` | Layout (has `<Outlet />`) |
- | `admin._index.tsx` | Index route (renders at parent URL) |
- | `admin.users.tsx` | Nested child route |
- | `admin_.settings.tsx` | Escapes parent layout with trailing underscore |
- | `_auth.tsx` | Pathless layout with leading underscore |
+ | File | Role |
+ | --------------------- | ------------------------------------------ |
+ | `admin.tsx` | Layout — renders `<Outlet />` |
+ | `admin._index.tsx` | What renders at `/admin` itself |
+ | `admin.users.tsx` | A child route |
+ | `admin_.settings.tsx` | `/admin/settings` without the admin layout |
+ | `_auth.tsx` | A layout that contributes no URL segment |
- See [examples/nested-routes.md](examples/nested-routes.md) for admin layout and pathless layout examples.
+ Full code: [examples/nested-routes.md](examples/nested-routes.md)
</patterns>
---
- **Detailed Resources:**
-
- - [examples/core.md](examples/core.md) - File-based routing, route naming, essential hooks
- - [examples/loaders.md](examples/loaders.md) - Protected routes, pagination, caching headers
- - [examples/actions.md](examples/actions.md) - Signup forms, validation, delete with confirmation
- - [examples/forms.md](examples/forms.md) - Multiple forms in one route, intent pattern
- - [examples/nested-routes.md](examples/nested-routes.md) - Layouts, pathless routes, admin panels
- - [examples/error-handling.md](examples/error-handling.md) - Multi-status error boundaries
- - [examples/optimistic.md](examples/optimistic.md) - Optimistic UI, debounced search
- - [examples/deferred.md](examples/deferred.md) - Streaming with defer/Promises
- - [examples/resource-routes.md](examples/resource-routes.md) - API endpoints, webhooks, file downloads
- - [examples/meta.md](examples/meta.md) - SEO meta tags, Open Graph, Twitter Cards
- - [examples/react-router-v7.md](examples/react-router-v7.md) - Migration: routes.ts, type generation, clientAction, Single Fetch
- - [reference.md](reference.md) - Decision frameworks, anti-patterns, route module exports
-
- ---
-
<red_flags>
- ## RED FLAGS
-
- **High Priority Issues:**
-
- - Loaders/actions exported from non-route files -- Remix only runs these from route modules
- - Missing type inference -- always use `useLoaderData<typeof loader>()` or `Route.ComponentProps`
- - Client-side data fetching with useEffect + fetch -- use loaders for server data
- - Returning null from loader instead of throwing Response -- every consumer must null-check
+ ## Red flags
- **Medium Priority Issues:**
+ **Breaks at runtime:**
- - Streaming critical data (page title, auth state) -- causes content flicker
- - useFetcher without optimistic UI -- makes interactions feel slow
- - Magic numbers for HTTP status codes -- use named constants
- - Form without `method="post"` -- defaults to GET, action not called
+ - A `loader` or `action` exported from a file that is not a route module — nothing calls it
+ - `<Form>` with no `method="post"` — it submits as a GET, so the action never runs
+ - `defer()` without a `<Suspense>` and `<Await>` around the consumer
+ - A form action targeting an index route without `?index` — the parent's action receives it
+ - `meta` reading `data` without a null branch — `data` is `undefined` when the loader threw
+ - A secret in a module a route component imports — route modules are bundled for the browser, unlike
+ loaders and actions
- **Gotchas & Edge Cases:**
+ **Surprising behaviour:**
- - Loader runs on every navigation -- even for child route changes, parent loaders re-run (use `shouldRevalidate` to optimize)
- - Action runs before all loaders -- after action, all loaders revalidate by default
- - `defer()` requires `<Suspense>` + `<Await>` wrapper -- forgetting causes errors
- - Index routes need `?index` query param for form actions targeting them
- - `meta` function receives null data on error -- must handle missing data case
- - `links` function cannot access loader data -- use `meta` with `tagName: "link"` for dynamic links
- - In React Router v7, `clientAction` takes priority when both `action` and `clientAction` exist -- server action is completely skipped
+ - Every parent loader re-runs on a child navigation, so an expensive parent query runs far more often
+ than expected — `shouldRevalidate` is the control
+ - All loaders revalidate after an action, whether or not they relate to what it changed
+ - `links` cannot access loader data, so a dynamic stylesheet URL has to go through `meta`
+ - On React Router v7, `clientAction` takes priority when both exist, and the server `action` is
+ skipped unless the `clientAction` calls it
+ - Returning `null` from a loader compiles and type-checks, and pushes the failure to whatever renders
+ it
+ - A `useFetcher` with no optimistic read of `fetcher.formData` shows nothing until the round trip
+ finishes
</red_flags>
-
- ---
-
- <critical_reminders>
-
- ## CRITICAL REMINDERS
-
- > **All code must follow project conventions in CLAUDE.md**
-
- **(You MUST export loaders and actions as named exports from route modules only -- they do not work in non-route files)**
-
- **(You MUST throw Response objects for expected errors (404, 403) -- use ErrorBoundary for handling)**
-
- **(You MUST await critical data and return non-critical data as Promises for streaming)**
-
- **(You MUST use named constants for HTTP status codes -- no magic numbers)**
-
- **Failure to follow these rules will break data loading, type safety, and error handling.**
-
- </critical_reminders>