web-meta-framework-remix · git:20260906.ae0cc61 · 2026-09-06 · sha256 2c87b5b1e0a92284
web-meta-framework-remix git:20260906.ae0cc61A
Immutable. This exact content is served forever at /api/v1/blob/2c87b5b1e0a92284.
---
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:** 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
---
## Which path applies
- **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).
---
<migration_notice>
## Remix v2 to React Router v7
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>
## Before writing Remix code
**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.
**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.
**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.
**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:** 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/
**Applies to:**
- 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
**Handled elsewhere:**
- 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 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.
Four things follow:
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 -> loaders run in parallel -> component renders -> user submits
|
action runs -> loaders revalidate
```
</philosophy>
---
<patterns>
## Core patterns
### Pattern 1: File-Based Routing
Files in `app/routes/` become URLs; the naming characters control nesting and dynamic segments.
| 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
const HTTP_NOT_FOUND = 404;
export async function loader({ params }: LoaderFunctionArgs) {
const post = await getPostBySlug(params.slug);
if (!post) throw new Response("Not Found", { status: HTTP_NOT_FOUND });
return { post };
}
```
Full code: [examples/core.md](examples/core.md) and
[examples/nested-routes.md](examples/nested-routes.md)
### Pattern 2: Loaders
A loader runs on the server for the initial render and over fetch on every client navigation
afterwards.
```typescript
export async function loader({ params, request }: LoaderFunctionArgs) {
const user = await getUser(params.userId);
if (!user) {
throw json({ message: "User not found" }, { status: HTTP_NOT_FOUND });
}
return json({ user });
}
```
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
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();
switch (formData.get("intent")) {
case "update":
return json({ success: true });
case "delete":
return redirect("/items");
default:
throw new Error(`Unknown intent`);
}
}
```
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
Await what the page needs; hand back the rest as Promises.
```typescript
// Remix v2
return defer({ user, analytics: getAnalytics() });
// React Router v7 — Single Fetch streams a raw Promise
return { user, analytics: getAnalytics() };
```
```tsx
<Suspense fallback={<Skeleton />}>
<Await resolve={analytics} errorElement={<p>Failed to load</p>}>
{(data) => <Chart data={data} />}
</Await>
</Suspense>
```
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
A fetcher submits or loads without navigating, which is what inline interactions need.
```typescript
const fetcher = useFetcher();
// Optimistic UI: the in-flight submission is already in fetcher.formData
const optimisticIsLiked = fetcher.formData
? fetcher.formData.get("liked") === "true"
: isLiked;
```
`<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
An exported `ErrorBoundary` catches everything below it in the route tree, and the first branch tells
a thrown Response from an unexpected exception.
```tsx
export function ErrorBoundary() {
const error = useRouteError();
if (isRouteErrorResponse(error)) {
return (
<div role="alert">
<h1>{error.status}</h1>
</div>
);
}
return (
<div role="alert">
<h1>Unexpected Error</h1>
</div>
);
}
```
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
`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 },
];
};
```
`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
A route module with no default export renders nothing and returns whatever its loader or action
does.
```typescript
// app/routes/api.health.ts
export async function loader() {
return json({ status: "healthy", timestamp: new Date().toISOString() });
}
```
Full code: [examples/resource-routes.md](examples/resource-routes.md)
### Pattern 9: Nested Routes
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.
| 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 |
Full code: [examples/nested-routes.md](examples/nested-routes.md)
</patterns>
---
<red_flags>
## Red flags
**Breaks at runtime:**
- 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
**Surprising behaviour:**
- 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>