Remix v2 Data Flow Code Review
Targets TypeScript route modules importing from @remix-run/*. See beagle-react: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 beagle-core: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 → beagle-react:remix-v2-data-flow
- Pre-report verification checklist → beagle-core:review-verification-protocol
Before Submitting Findings
Complete Hard gates (especially gate 5), then report only issues that still pass the beagle-core:review-verification-protocol pre-report checks.