Skill: anderskev/remix-v2-data-flow (version 1.0.0)

--- name: remix-v2-data-flow description: Remix v2 data loading and mutations. Use when writing loaders, actions, deferred data, revalidation logic, or pending state. Triggers on loader, action, useLoaderData, useActionData, json(), defer(), <Await>, shouldRevalidate, useRevalidator, useNavigation, useTransition (v1 holdover). --- # Remix v2 Data Flow ## Quick Reference **Loader + typed read**: ```tsx import { json, type LoaderFunctionArgs } from "@remix-run/node"; import { useLoaderData } from "@remix-run/react"; export async function loader({ request }: LoaderFunctionArgs) { const invoices = await db.invoice.findMany(); return json({ invoices }); } export default function Invoices() { // typeof loader is a type ANNOTATION (not assertion) — drives SerializeFrom<T>. const { invoices } = useLoaderData<typeof loader>(); return <InvoiceList invoices={invoices} />; } ``` **Action + redirect-after-success (PRG)**: ```tsx import { json, redirect, type ActionFunctionArgs } from "@remix-run/node"; import { useActionData, Form } from "@remix-run/react"; export async function action({ request }: ActionFunctionArgs) { const form = await request.formData(); const parsed = NewProject.safeParse(Object.fromEntries(form)); if (!parsed.success) return json({ errors: parsed.error.flatten().fieldErrors }, { status: 400 }); const project = await db.project.create({ data: parsed.data }); return redirect(`/projects/${project.id}`); } ``` ## Canonical APIs Route modules export `loader` / `action`; components read results via `useLoaderData<typeof loader>()` and `useActionData<typeof action>()`. After every action, Remix automatically revalidates the loaders of all matching routes on the page, so the UI stays consistent with the server without manual cache invalidation. Signatures: - `loader: ({ request, params, context }: LoaderFunctionArgs) => Response | Promise<Response>` — server-only read, runs on SSR and on client navigations via fetch. - `action: ({ request, params, context }: ActionFunctionArgs) => Response | Promise<Response>` — server-only handler for non-GET requests (POST/PUT/PATCH/DELETE). - `json(data, init?: number | ResponseInit): TypedResponse<typeof data>` — ergonomic JSON `Response` wrapper with status/headers. - `redirect(url, init?: number | ResponseInit): TypedResponse<never>` — 30x response; default 302. Imports: `@remix-run/node` for server utilities (`json`, `redirect`, `defer`, type args) on Node; substitute `@remix-run/cloudflare` or `@remix-run/deno` for those targets. Hooks and components come from `@remix-run/react`. ## Type Annotations, Not Assertions `useLoaderData<typeof loader>()` is a **type annotation**, not a `as`-style assertion. The generic feeds `SerializeFrom<typeof loader>`, which models the wire-format transformation: `Date` becomes `string`, `Map`/`Set` collapse, `undefined` fields are stripped, class methods vanish. If you call `data.createdAt.getFullYear()` on a `Date` field, that's a runtime bug — the type already says `string`. ## When `json()` Is Optional in v2 v2 did **not** change the underlying contract: loaders and actions must return a `Response`. `json()` is the ergonomic wrapper that sets `application/json` and lets you supply status / headers. Bare object returns work in v2 (Remix auto-wraps as `json()`), but `json()` is preferred for explicit status, headers, and clean `TypedResponse<T>` typing. Reach for `json()` whenever you need: - A non-200 status code (e.g. `{ status: 400 }` for validation errors). - Custom headers (caching, `Set-Cookie`). - An explicit `TypedResponse<T>` for clean `useLoaderData<typeof loader>()` inference. ## Throwing for Short-Circuits Throwing a `Response` from a loader or action exits the data function immediately. Use this for auth guards (`throw redirect("/login")`) and 404s (`throw new Response("Not Found", { status: 404 })` or `throw json({ message }, { status: 404 })`). Throwing a plain `Error` will not be classified as a route response by `useRouteError()` / `isRouteErrorResponse()`. ## Streaming Rejections: `<Await errorElement>` + `useAsyncError()` When a promise passed through `defer()` rejects, an `<Await errorElement={...}>` boundary catches it inline — without it, the rejection bubbles to the route's `ErrorBoundary` and tears down the whole page, defeating the streaming benefit. Inside the `errorElement`, call `useAsyncError()` (from `@remix-run/react`) to read the rejection value — this is the streaming analogue of `useRouteError()`. ```tsx function ReviewsError() { const error = useAsyncError(); // typed as `unknown` return <p>Failed to load reviews: {String(error)}</p>; } <Suspense fallback={<ReviewsSkeleton />}> <Await resolve={reviews} errorElement={<ReviewsError />}> {(r) => <ReviewList reviews={r} />} </Await> </Suspense> ``` ## Sensitive Data Everything returned from a loader travels to the browser as JSON. Project to a safe DTO (`{ id, email, name }`) before returning; never return the full Prisma `User`, password hashes, API keys, or internal flags. Loaders execute server-only — but the *return value* is shipped to the client wholesale. ## Mutations Belong in Actions Loaders run on every GET navigation and may be invoked speculatively by prefetch; they also re-run during automatic revalidation. Anything that mutates persistent state must live in `action`, reached via `<Form method="post">` or `useFetcher`. Calling `fetch()` directly from a component to hit a Remix route bypasses revalidation, pending state, and progressive enhancement — use `useFetcher().submit()` / `useFetcher().load()` instead. ## Routing Gotchas to Remember - **Only the deepest matching action runs.** Index routes nested under a layout collide unless you target them with `<Form action="/things?index" method="post" />`. - **`useActionData` is scoped to the current route.** It cannot access action results from parent or child routes; to share, lift the action or use a `useFetcher` with `key`. - **Headers from the leaf loader win.** Remix only uses the deepest matching `headers` export. Parent caching policies are ignored unless you explicitly merge `parentHeaders`. - **Default revalidation revalidates ALL routes on the page after an action** — even those whose params didn't change. Use `shouldRevalidate` to opt out of expensive parent loaders. ## Pending State (v1 → v2) `useTransition` is removed in v2 — use `useNavigation`. The `submission` object is flattened directly onto the navigation in v2 (and the fetcher likewise; both `nav.formData`/`nav.formMethod` and `fetcher.formData`/`fetcher.formMethod` are flat). `formMethod` is now **UPPERCASE** in v2 (`"POST"`, not `"post"`); comparisons like `nav.formMethod === "post"` silently never match. `fetcher.type` is also gone — branch on `fetcher.state` plus presence of `fetcher.formData`. GET submissions go `idle → loading → idle`. POST flow goes `idle → submitting → loading → idle`. Spinners gated only on `"submitting"` will miss GET forms. GET submissions still populate `nav.formData` and `nav.formMethod === "GET"` during the `loading` phase, so filter-form pending UI should branch on `formData` presence, not on `state === 'submitting'`. For `useFetcher`, `submitting` applies to BOTH GET (`<fetcher.Form method='get'>` and `fetcher.submit(..., {method:'get'})`) and non-GET; only `fetcher.load()` skips `submitting`. This is the inverse of `useNavigation`, which skips `submitting` for GET. ## Gates (decision sequencing) Answer **in order**. **Pass** means the condition is true; pick the API on the same line and **stop**. ### `loader` vs `useEffect` 1. **Is the data needed for correct first render** of this route (SSR, prefetch, automatic revalidation after actions)? - **Pass →** `loader` + `useLoaderData<typeof loader>()`. **Stop.** - **Fail →** Step 2. 2. **Is the fetch driven by post-mount user interaction, timer, or subscription** (not route entry)? - **Pass →** `useEffect` / event handlers. **Stop.** - **Fail →** Prefer loader + revalidation; do not mirror navigation inside an effect. ### `json()` vs raw `Response` vs `defer()` 1. **Do any returned fields need to stream** (slow query, expensive aggregation) while the page renders fast? - **Pass →** `defer({ critical: await…, slow: promiseWithoutAwait })` + `<Suspense><Await>…</Await></Suspense>`. **Stop.** - **Fail →** Step 2. 2. **Do you need a custom status code, custom headers, or explicit `TypedResponse<T>` typing**? - **Pass →** `json(data, init)` (or `redirect(url, init)` for 3xx). **Stop.** - **Fail →** Step 3. 3. **Do you need a non-JSON body** (binary, plain text, streamed file)? - **Pass →** Build a raw `new Response(body, init)`. **Stop.** - **Fail →** Default to `json(data)` — it's the documented v2 contract for object payloads. ### `<Form>` / route action vs `useFetcher` 1. **Should the URL or history stack change** (bookmark / share / back returns to prior screen)? - **Pass →** `<Form method="post">` posting to a route `action`. **Stop.** - **Fail →** Step 2. 2. **Mutation stays on the same route** (inline edit, list-row toggle, popover, optimistic UI)? - **Pass →** `useFetcher()` / `fetcher.Form` / `fetcher.submit()`. **Stop.** ## Additional Documentation - **Loaders**: See [references/loaders.md](references/loaders.md) for `loader` signature, typed `useLoaderData`, `json()` vs raw `Response`, `redirect()`, throwing, sensitive-data filtering, params handling. - **Actions**: See [references/actions.md](references/actions.md) for `action` signature, FormData parsing, `useActionData<typeof action>()`, zod/valibot validation, redirect-after-success. - **Defer & Await**: See [references/defer-await.md](references/defer-await.md) for `defer()` + `<Await>` + `<Suspense>`, when streaming helps TTFB, error handling. - **Revalidation & Pending State**: See [references/revalidation.md](references/revalidation.md) for automatic revalidation, `shouldRevalidate`, `useRevalidator`, `useNavigation` (plus v1 `useTransition` rename). ## v1 → v2 Quick Diff | Concern | v1 | v2 | |----------------------|-------------------------------------|--------------------------------------------------| | Navigation hook | `useTransition()` | `useNavigation()` | | Submission shape | `transition.submission.formMethod` | flat `nav.formMethod` / `nav.formData` | | `formMethod` casing | `"post"` | `"POST"` (UPPERCASE) | | Fetcher type field | `fetcher.type === "actionSubmission"` | branch on `fetcher.state` + `fetcher.formData` | | Loader args type | `LoaderArgs` / `ActionArgs` | `LoaderFunctionArgs` / `ActionFunctionArgs` | | Returning data | `json(data)` required | `json(data)` still the documented contract |


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