Remix v2 Data Flow Code Review
SkillDev toolsReviews 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.
Available today. Use it from your connected AI after setup.
No other account needed.
Connect ahel once, and every AI you use reads what you have installed.
Then ask your AI: use the Remix v2 Data Flow Code Review skill
What this skill tells your AI
The instructions your AI receives, as published by existential-birds/beagle in plugins/beagle-react/skills/remix-v2-data-flow-review/SKILL.md and read by ahel’s review.
Targets TypeScript route modules importing from @remix-run/*. See remix-v2-data-flow for canonical patterns.
Scope
- In scope: route modules under
app/routes/exportingloader,action,shouldRevalidate, orheaders; components that consumeuseLoaderData,useActionData,useNavigation,useFetcher,useRevalidator,<Await>. - Out of scope: form ergonomics (
<Form>markup, accessibility,useFetcherUI patterns) → covered byremix-v2-forms-review. Route module conventions, file naming, nested routing, error boundary placement → covered byremix-v2-routing-review. - Imports expected:
@remix-run/node(or@remix-run/cloudflare/@remix-run/deno) for server utilities;@remix-run/reactfor hooks and components.
Quick Reference
| Issue Type | Reference |
|---|---|
| Mutations in loader, missing validation, leaked server fields, throwing primitives, missing param checks | references/loaders.md |
Unvalidated FormData, json instead of redirect on success, missing error case, leaked actionData | references/actions.md |
useTransition v1 holdover, missing pending state, blanket shouldRevalidate: false, misused useRevalidator | references/revalidation.md |
defer for already-fast data, missing <Suspense>, no errorElement on <Await>, awaiting what should stream | references/defer-await.md |
Review Checklist
- Data needed for first render is in
loader, notuseEffect - 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 (notas Foo) - 404 / auth short-circuits
throwaResponse(orjson/redirect), never a plainErroror string - Successful action returns
redirect(...)(PRG); validation failures returnjson({ errors }, { status: 400 }) - Action handles both success and error branches; no silent
return null -
params.foois checked withinvariant/ zod before use - Pending UI reads
useNavigation()/fetcher.state— nouseTransition -
formMethodcomparisons use UPPERCASE ("POST", not"post") -
shouldRevalidatereturnsdefaultShouldRevalidateby default; opt-outs are narrow and justified -
defer()is used only when at least one promise streams (noawaitbefore passing it) - Every
<Await>is wrapped in<Suspense>and has anerrorElement -
useRevalidator().revalidate()is reserved for focus/polling/SSE — not called immediately after a<Form>post orfetcher.submit(Remix already revalidates).
Valid Patterns (Do NOT Flag)
These are correct Remix v2 usage and must not be reported as issues:
useEffectfor client-only data — Loaders run server-side;localStorage,windowdimensions,IntersectionObserver, and browser-only APIs belong inuseEffect.loaderreturningnull— A loader may legitimately returnnull(e.g. optional resource not present); flag only if it should be a 404throw.useLoaderData<typeof loader>()as type annotation — The<typeof loader>is a generic parameter feedingSerializeFrom<T>, not aas-style type assertion. Do not flag it as "unsafe cast."- Bare
new Response(body, init)returns — v2 routes may return anyResponse;json()is an ergonomic wrapper, not a requirement. Non-JSON bodies (binary, text, streams) correctly skipjson(). return redirect(...)from an action — Bothreturn redirect(...)andthrow redirect(...)are legal in actions; throwing is required only from non-action helpers when you want to exit the calling function.loaderdeclared without therequestarg — Loaders may destructure only what they need ({ params },{ context }, or()with no args); the unused arg is not a bug.- Parent
loaderrevalidated after an unrelated action — This is default Remix behavior, not a smell. Flag only ifshouldRevalidateexists 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. useRevalidatorfor focus / polling / cross-tab sync — These are the documented use cases; only flag manualrevalidate()calls that immediately follow a<Form>post orfetcher.submitRemix would already revalidate.SerializeFrom-induced type changes —Datetyped asstring,Maptyped 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 |
Manual useRevalidator().revalidate() | The call follows a Remix-managed mutation (<Form> post, fetcher.submit) — not focus / polling / websocket |
defer() used | Every promise in the defer({...}) payload was already awaited before the call |
Hard gates (before writing findings)
Run these in order. Do not draft user-facing findings until every gate passes for the batch you are about to report.
-
Location evidence — Pass: Each issue lists the repo path to the route module and either a line range or a short verbatim quote from the file you read (not from memory or diff-only guesswork). Loader/action issues without a path to the
export async function loader|actionare not reportable. -
Exemption check — Pass: For each issue, you can state in one line why it is not covered by Valid Patterns (Do NOT Flag). In particular: confirm
useEffectis not loading client-only data; confirm a bareResponsereturn is not intentionally non-JSON; confirm aloaderreturningnullis not a legitimate optional read. -
Type-annotation vs type-assertion check — Pass: Before flagging an "unsafe cast" on loader/action consumption, confirm the code uses
as(assertion) — notuseLoaderData<typeof loader>()(annotation) and notuseActionData<typeof action>()(annotation). The generic form is the documented safe path and must not be flagged. -
v1 holdover check — Pass: Before flagging "missing pending state," grep the file for
useTransition,transition.submission,fetcher.type,formMethod === "post"orformMethod==='post'(lowercase, any whitespace/quote variation), andLoaderArgs/ActionArgs. If present, the finding is a v1-holdover migration issue, not a missing-feature issue — label it accordingly. -
Protocol — Pass: You completed the Pre-Report Verification Checklist in review-verification-protocol for this review.
When to Load References
- Reviewing a
loaderbody, return shape, params, throws, or sensitive-field leaks → references/loaders.md - Reviewing an
actionbody, FormData validation, success/error branches, or PRG redirect → references/actions.md - Reviewing
useNavigation/useTransitionmigrations,shouldRevalidate, oruseRevalidatoruse → references/revalidation.md - Reviewing
defer(),<Await>,<Suspense>, or streaming decisions → references/defer-await.md
Review Questions
- Is data needed for first render fetched in a
loader, or is it stuck in auseEffectthat defeats SSR and revalidation? - Does every loader return a projected DTO, or do raw ORM records (with
password,token,internal_*fields) leak to the browser? - Does every action validate
request.formData()with a schema before touching the database? - Does the success branch of each action
redirect(...)so refresh / back behaves correctly (PRG)? - Is the consumer code using
useLoaderData<typeof loader>()(annotation) — notuseLoaderData() as Foo(assertion)? - Do any v1 holdovers remain (
useTransition,transition.submission,fetcher.type, lowercaseformMethod,LoaderArgs/ActionArgs)? - Does
shouldRevalidatereturn a literalfalse, or does it reach fordefaultShouldRevalidateand opt out narrowly? - Is
defer()used only when at least one promise is passed unresolved, and is every<Await>wrapped in<Suspense>with anerrorElement?
Additional Documentation
- Canonical Remix v2 data-flow patterns and v1 → v2 diff → remix-v2-data-flow
- Pre-report verification checklist → review-verification-protocol
Before Submitting Findings
Complete Hard gates (especially gate 5), then report only issues that still pass the review-verification-protocol pre-report checks.
Signals
- GitHub stars
- 81
- Forks
- 8
- Last commit
- Aug 2026
Advanced
- Catalog kind
- skill
- Gateway key
remix-v2-data-flow-review- Source
- github.com/existential-birds/beagle