Remix v2 Forms Code Review
SkillDev toolsReviews Remix v2 form code for manual fetch() mutations, native <form> misuse, wrong useNavigation/useFetcher choice, missing pending state, unbounded uploads, and intent-pattern violations. Use when reviewing form/mutation 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 Forms 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-forms-review/SKILL.md and read by ahel’s review.
See remix-v2-forms 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 |
useState loading flags, useNavigation for per-row, missing pending state | references/pending-state.md |
Unbounded memory uploads, missing encType, unvalidated FormData, mirrored optimistic state | references/uploads-validation.md |
| Route sprawl instead of intent pattern, PUT/DELETE without PE fallback | 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) and any matching row in 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 for this review.
Additional Documentation
- Form vs fetcher misuse — manual
fetch(), native<form>, wrong primitive: references/form-vs-fetcher.md - Pending state anti-patterns —
useStateflags, page-global vs per-row, missing entirely: references/pending-state.md - Uploads & FormData validation — unbounded handlers, missing
encType, unvalidated keys, mirrored optimistic state: references/uploads-validation.md - Multi-action routes — intent-pattern violations, PUT/DELETE without PE fallback, missing submit button: references/multi-action-routes.md
- Canonical patterns — see sibling remix-v2-forms
When to Load References
- Reviewing forms that call
fetch()/axios/ native<form>, or choose between<Form>anduseFetcher→ form-vs-fetcher.md - Reviewing loading flags, spinners, disabled-button logic, per-row pending → pending-state.md
- Reviewing file uploads,
unstable_*handlers, FormData parsing, optimistic UI → uploads-validation.md - Reviewing routes with multiple mutations, intent fields, PUT/DELETE methods → 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 pattern, not separate routes?
- Do POST forms have at least one real submit button for progressive enhancement?
False-Positive Notes
- A
<Form>rendering inside a non-route component is still tied to the nearest route'saction— read the route file before flagging "missing action". useActionData()returning data after a successful submission is expected behavior; the data persists until the next navigation. Only flag if a success banner is rendered unconditionally without a dismiss path.- Code that imports
Formaliased (e.g.import { Form as RemixForm }) is still the Remix component — match on import source, not local name.
Before Submitting Findings
Complete Hard gates (especially gate 4), 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-forms-review- Source
- github.com/existential-birds/beagle