Remix v2 Forms Code Review
See [beagle-react:remix-v2-forms](../remix-v2-forms/SKILL.md) for canonical patterns. This skill flags violations; the sibling skill teaches the patterns.
Quick Reference
| Issue Type | Reference | |------------|-----------| | Manual fetch(), native <form>, wrong <Form> vs useFetcher choice | [references/form-vs-fetcher.md](references/form-vs-fetcher.md) | | useState loading flags, useNavigation for per-row, missing pending state | [references/pending-state.md](references/pending-state.md) | | Unbounded memory uploads, missing encType, unvalidated FormData, mirrored optimistic state | [references/uploads-validation.md](references/uploads-validation.md) | | Route sprawl instead of intent pattern, PUT/DELETE without PE fallback | [references/multi-action-routes.md](references/multi-action-routes.md) |
Review Checklist
- [ ] In-app mutations use
<Form>or<fetcher.Form>, neverfetch()/axios - [ ]
<Form>is imported from@remix-run/react(not native<form>) for POST mutations - [ ]
useFetcherused when URL should NOT change (row toggle, inline edit) - [ ]
<Form>+redirect(...)used when URL SHOULD change (create, delete-then-list) - [ ] Pending state derived from
useNavigation()orfetcher.state, neveruseState - [ ] Per-row pending uses per-row
fetcher.state(not page-globaluseNavigation) - [ ]
useNavigation()calls checknavigation.formActionto scope to expected path - [ ] Optimistic UI reads
fetcher.formData/navigation.formDatadirectly (not mirrored) - [ ] Actions returning success
redirect(...), returningjson()only for errors / same-page - [ ]
<Form encType="multipart/form-data">on every file-upload form - [ ]
unstable_createMemoryUploadHandleralways hasmaxPartSize; large files use disk/stream handler - [ ]
FormDatavalues are validated/coerced before reaching the DB (noform.get(x) as string) - [ ] Multiple mutations on one route use intent pattern, not separate routes
- [ ]
method="put|patch|delete"is documented as JS-only, or rewritten as POST + intent - [ ]
nav.formMethod/fetcher.formMethodcompared against UPPERCASE strings ("POST","GET"); v2'sv2_normalizeFormMethoddefault returns UPPERCASE —=== "post"silently never matches.
Valid Patterns (Do NOT Flag)
These are correct Remix v2 usage and should not be reported:
<Form>withoutactionprop — posts to the current URL by convention; explicitactionis optional.- GET
<Form>— legitimate for search/filter UIs; hits the loader with form fields as URL search params and does NOT call an action. Most "hygiene" rules (intent,redirect,encType) apply only to POST forms. - Multiple
useFetcher()instances on one page — each call returns an independent submission channel; intentional for parallel mutations to different rows. useSubmit()in an event handler — correct programmatic submission for autosave, keyboard shortcuts, oronChangetriggers.- Reading
fetcher.formDataduring a submission — intended; this is the canonical optimistic source. useActionDatadata persisting after submission — known behavior; it returns the last action result until the next navigation or action.navigate={false}on<Form>— turns it into a fetcher form; equivalent to<fetcher.Form>without holding a fetcher ref.unstable_prefix onparseMultipartFormData/ upload handlers — permanent in v2; do not flag as "unstable API".
Context-Sensitive Rules
Only flag these when the listed condition holds:
| Issue | Flag ONLY IF | |-------|--------------| | Native <form> instead of <Form> | Method is POST and the route has an action — GET forms and external-URL forms are fine | | Missing pending state | The form is POST and there is no useNavigation() / fetcher.state read anywhere in the component | | Action returns json({ ok: true }) after a create | The route is a "/new" or creation surface — same-page edit forms legitimately return JSON | | method="put" / "patch" / "delete" | Progressive enhancement is in scope for the surface (public app) — admin/JS-only tools may opt out if documented | | Unbounded unstable_createMemoryUploadHandler | The upload accepts user-controlled files (not a fixed-size internal artifact) | | Separate routes per mutation | The mutations operate on the same resource with compatible auth — sibling resources with different rules are fine | | useNavigation() without formAction filter | The component contains other navigation surfaces (sidebar <Link>, sibling forms) that would trigger false positives | | Mirroring fetcher.formData into state | The shadowed value drives a user-visible element (button label, count, toggle) — a local "is-editing" flag is unrelated |
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 a repo path and either a line range or a short verbatim quote from the file you read (not from memory or diff-only guesswork). Name the route module, the component, and the
actionif one exists.
- Exemption check — Pass: For each issue, you can state in one line why it is not covered by [Valid Patterns (Do NOT Flag)](#valid-patterns-do-not-flag) and any matching row in [Context-Sensitive Rules](#context-sensitive-rules).
- Form-method check — Pass: Before flagging missing intent, missing
encType, missingredirect, or missing pending state, you have confirmed the form ismethod="post"(orput|patch|delete). GET forms are legitimate for search/filter and trigger loaders, not actions — applying POST-form rules to them is a false positive.
- Protocol — Pass: You completed the Pre-Report Verification Checklist in [review-verification-protocol](../../../beagle-core/skills/review-verification-protocol/SKILL.md) for this review.
Additional Documentation
- Form vs fetcher misuse — manual
fetch(), native<form>, wrong primitive: [references/form-vs-fetcher.md](references/form-vs-fetcher.md) - Pending state anti-patterns —
useStateflags, page-global vs per-row, missing entirely: [references/pending-state.md](references/pending-state.md) - Uploads & FormData validation — unbounded handlers, missing
encType, unvalidated keys, mirrored optimistic state: [references/uploads-validation.md](references/uploads-validation.md) - Multi-action routes — intent-pattern violations, PUT/DELETE without PE fallback, missing submit button: [references/multi-action-routes.md](references/multi-action-routes.md)
- Canonical patterns — see sibling [beagle-react:remix-v2-forms](../remix-v2-forms/SKILL.md)
When to Load References
- Reviewing forms that call
fetch()/axios/ native<form>, or choose between<Form>anduseFetcher→ [form-vs-fetcher.md](references/form-vs-fetcher.md) - Reviewing loading flags, spinners, disabled-button logic, per-row pending → [pending-state.md](references/pending-state.md)
- Reviewing file uploads,
unstable_*handlers, FormData parsing, optimistic UI → [uploads-validation.md](references/uploads-validation.md) - Reviewing routes with multiple mutations, intent fields, PUT/DELETE methods → [multi-action-routes.md](references/multi-action-routes.md)
Review Questions
- Does every in-app mutation flow through a route
action(no manualfetch())? - Is the
<Form>vsuseFetcherchoice driven by whether the URL should change? - Is pending state derived from
useNavigation()/fetcher.state(neveruseState)? - Are per-row spinners wired to per-row
fetcher.state(not page-globaluseNavigation)? - Do file-upload forms set
encType="multipart/form-data"and use bounded handlers? - Are FormData values validated before reaching the DB?
- Do multiple mutations on one resource use the intent