Skip to content
Development
Skill

/remix-v2-data-flow-review

Reviews Remix v2 loaders and actions for mutations-in-loader, missing validation, leaked server fields, wrong return helpers, v1 useTransition holdovers, and revalidation traps. Use when reviewing loader/action code in a Remix v2 codebase.

From plugin
beagle
82139 skills2 commands
Install
$ npx -y skills add existential-birds/beagle --skill remix-v2-data-flow-review --agent claude-code

How it fires

How this skill gets triggered: by you, by Claude, or both.

  • Fires itselfAuto-invocation. Claude auto-loads it when your prompt matches the work.Auto-invocation is when the right skill fires by itself at the right moment, driven by a FLOW.md router and a hook, instead of you invoking it by name. It is the difference between a skill being installed and a skill actually getting used.Read the full definition →
  • You can call itInvoke it directly when you want it.
  • Slash command/remix-v2-data-flow-review

Context preview

The summary Claude sees to decide when to auto-load this skill.

Reviews Remix v2 loaders and actions for mutations-in-loader, missing validation, leaked server fields, wrong return helpers, v1 useTransition holdovers, and revalidation traps. Use when reviewing loader/action code in a Remix v2 codebase.

SKILL.md

remix-v2-data-flow-review.SKILL.md
name: remix-v2-data-flow-review
description: Reviews Remix v2 loaders and actions for mutations-in-loader, missing validation, leaked server fields, wrong return helpers, v1 useTransition holdovers, and revalidation traps. Use when reviewing loader/action code in a Remix v2 codebase.

Remix v2 Data Flow Code Review

Targets TypeScript route modules importing from `@remix-run/*`. See [remix-v2-data-flow](../remix-v2-data-flow/SKILL.md) for canonical patterns.

Scope

  • **In scope**: route modules under `app/routes/` exporting `loader`, `action`, `shouldRevalidate`, or `headers`; components that consume `useLoaderData`, `useActionData`, `useNavigation`, `useFetcher`, `useRevalidator`, `<Await>`.
  • **Out of scope**: form ergonomics (`<Form>` markup, accessibility, `useFetcher` UI patterns) → covered by `remix-v2-forms-review`. Route module conventions, file naming, nested routing, error boundary placement → covered by `remix-v2-routing-review`.
  • **Imports expected**: `@remix-run/node` (or `@remix-run/cloudflare` / `@remix-run/deno`) for server utilities; `@remix-run/react` for hooks and components.

Quick Reference

| Issue Type | Reference | |------------|-----------| | Mutations in loader, missing validation, leaked server fields, throwing primitives, missing param checks | [references/loaders.md](references/loaders.md) | | Unvalidated FormData, `json` instead of `redirect` on success, missing error case, leaked actionData | [references/actions.md](references/actions.md) | | `useTransition` v1 holdover, missing pending state, blanket `shouldRevalidate: false`, misused `useRevalidator` | [references/revalidation.md](references/revalidation.md) | | `defer` for already-fast data, missing `<Suspense>`, no `errorElement` on `<Await>`, awaiting what should stream | [references/defer-await.md](references/defer-await.md) |

Review Checklist

  • [ ] Data needed for first render is in `loader`, not `useEffect`
  • [ ] Loaders only read; writes live in `action`
  • [ ] `request.formData()` results are validated (zod/valibot/invariant) before use
  • [ ] Loader/action return values are projected DTOs — no password hashes, tokens, or `internal_*` fields
  • [ ] `useLoaderData<typeof loader>()` uses the type annotation form (not `as Foo`)
  • [ ] 404 / auth short-circuits `throw` a `Response` (or `json`/`redirect`), never a plain `Error` or string
  • [ ] Successful action returns `redirect(...)` (PRG); validation failures return `json({ errors }, { status: 400 })`
  • [ ] Action handles both success and error branches; no silent `return null`
  • [ ] `params.foo` is checked with `invariant` / zod before use
  • [ ] Pending UI reads `useNavigation()` / `fetcher.state` — no `useTransition`
  • [ ] `formMethod` comparisons use UPPERCASE (`"POST"`, not `"post"`)
  • [ ] `shouldRevalidate` returns `defaultShouldRevalidate` by default; opt-outs are narrow and justified
  • [ ] `defer()` is used only when at least one promise streams (no `await` before passing it)
  • [ ] Every `<Await>` is wrapped in `<Suspense>` and has an `errorElement`
  • [ ] `useRevalidator().revalidate()` is reserved for focus/polling/SSE — not called immediately after a `<Form>` post or `fetcher.submit` (Remix already revalidates).

Valid Patterns (Do NOT Flag)

These are correct Remix v2 usage and must not be reported as issues:

  • **`useEffect` for client-only data** — Loaders run server-side; `localStorage`, `window` dimensions, `IntersectionObserver`, and browser-only APIs belong in `useEffect`.
  • **`loader` returning `null`** — A loader may legitimately return `null` (e.g. optional resource not present); flag only if it should be a 404 `throw`.
  • **`useLoaderData<typeof loader>()` as type annotation** — The `<typeof loader>` is a generic parameter feeding `SerializeFrom<T>`, not a `as`-style type assertion. Do not flag it as "unsafe cast."
  • **Bare `new Response(body, init)` returns** — v2 routes may return any `Response`; `json()` is an ergonomic wrapper, not a requirement. Non-JSON bodies (binary, text, streams) correctly skip `json()`.
  • **`return redirect(...)` from an action** — Both `return redirect(...)` and `throw redirect(...)` are legal in actions; throwing is required only from non-action helpers when you want to exit the calling function.
  • **`loader` declared without the `request` arg** — Loaders may destructure only what they need (`{ params }`, `{ context }`, or `()` with no args); the unused arg is not a bug.
  • **Parent `loader` revalidated after an unrelated action** — This is default Remix behavior, not a smell. Flag only if `shouldRevalidate` exists and is wrong.
  • **Action returning `json({ errors }, { status: 400 })`** — This is the canonical validation-error pattern (keeps the form route rendered with field errors). Not the same as the "no redirect on success" anti-pattern.
  • **`useRevalidator` for focus / polling / cross-tab sync** — These are the documented use cases; only flag manual `revalidate()` calls that immediately follow a `<Form>` post or `fetcher.submit` Remix would already revalidate.
  • **`SerializeFrom`-induced type changes** — `Date` typed as `string`, `Map` typed as `{}` after deserialization is correct wire-format behavior, not a typing bug.

Context-Sensitive Rules

Only flag these issues when the specific context applies:

| Issue | Flag ONLY IF | |-------|--------------| | Missing loader (using `useEffect` instead) | Data is available server-side and is NOT a browser-only API read | | `loader` returns a raw ORM object | The object contains fields a reviewer would not paste into a screenshot (passwords, tokens, internal flags) | | Action returns `json` on success | The action is invoked via `<Form>` causing a URL change — NOT via `useFetcher` | | Missing pending UI | No `nav.state` / `fetcher.state` reference exists elsewhere in the file driving the same surface | | `shouldRevalidate` returns `false` | The body has no condition or never references `formAction` / `currentParams` / `nextParams

Read more
Ships withbeagle

Image: NASA, Public Domain. Source Beagle is an Agent Skills marketplace: framework-aware code review, documentation, testing, architectural analysis, and git workflows for any compatible coding agent.

Get the whole plugin

Other skills on beagle.