审查 Remix v2 表单代码,针对手动 fetch() 变更、原生 <form> 误用、错误的 useNavigation/useFetcher 选择、缺少 pending 状态以及无限制上传等问题。
See remix-v2-forms for canonical patterns. This skill flags violations; the sibling skill teaches the patterns.
Issue TypeReferenceManual fetch(), native <form>, wrong <Form> vs useFetcher choicereferences/form-vs-fetcher.mduseState loading flags, useNavigation for per-row, missing pending statereferences/pending-state.mdUnbounded memory uploads, missing encType, unvalidated FormData, mirrored optimistic statereferences/uploads-validation.mdRoute sprawl instead of intent pattern, PUT/DELETE without PE fallbackreferences/multi-action-routes.md
<Form> or <fetcher.Form>, never fetch() / axios<Form> is imported from @remix-run/react (not native <form>) for POST mutationsuseFetcher used when URL should NOT change (row toggle, inline edit)<Form> + redirect(...) used when URL SHOULD change (create, delete-then-list)useNavigation() or fetcher.state, never useStatefetcher.state (not page-global useNavigation)useNavigation() calls check navigation.formAction to scope to expected pathfetcher.formData / navigation.formData directly (not mirrored)redirect(...), returning json() only for errors / same-page<Form encType="multipart/form-data"> on every file-upload formunstable_createMemoryUploadHandler always has maxPartSize; large files use disk/stream handlerFormData values are validated/coerced before reaching the DB (no form.get(x) as string)method="put|patch|delete" is documented as JS-only, or rewritten as POST + intentnav.formMethod / fetcher.formMethod compared against UPPERCASE strings ( "POST", "GET"); v2's v2_normalizeFormMethod default returns UPPERCASE — === "post" silently never matches.These are correct Remix v2 usage and should not be reported:
<Form> without action prop — posts to the current URL by convention; explicit action is optional.<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.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, or onChange triggers.fetcher.formData during a submission — intended; this is the canonical optimistic source.useActionData data 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 on parseMultipartFormData / upload handlers — permanent in v2; do not flag as "unstable API".Only flag these when the listed condition holds:
IssueFlag ONLY IFNative <form> instead of <Form>Method is POST and the route has an action — GET forms and external-URL forms are fineMissing pending stateThe form is POST and there is no useNavigation() / fetcher.state read anywhere in the componentAction returns json({ ok: true }) after a createThe route is a "/new" or creation surface — same-page edit forms legitimately return JSONmethod="put" / "patch" / "delete"Progressive enhancement is in scope for the surface (public app) — admin/JS-only tools may opt out if documentedUnbounded unstable_createMemoryUploadHandlerThe upload accepts user-controlled files (not a fixed-size internal artifact)Separate routes per mutationThe mutations operate on the same resource with compatible auth — sibling resources with different rules are fineuseNavigation() without formAction filterThe component contains other navigation surfaces (sidebar <Link>, sibling forms) that would trigger false positivesMirroring fetcher.formData into stateThe shadowed value drives a user-visible element (button label, count, toggle) — a local "is-editing" flag is unrelated
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 action if 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, missing redirect, or missing pending state, you have confirmed the form is method="post" (or put|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.
fetch(), native <form>, wrong primitive: references/form-vs-fetcher.mduseState flags, page-global vs per-row, missing entirely: references/pending-state.mdencType, unvalidated keys, mirrored optimistic state: references/uploads-validation.mdfetch() / axios / native <form>, or choose between <Form> and useFetcher → form-vs-fetcher.mdunstable_* handlers, FormData parsing, optimistic UI → uploads-validation.mdaction (no manual fetch())?<Form> vs useFetcher choice driven by whether the URL should change?useNavigation() / fetcher.state (never useState)?fetcher.state (not page-global useNavigation)?encType="multipart/form-data" and use bounded handlers?<Form> rendering inside a non-route component is still tied to the
nearest route's action — 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.Form aliased (e.g. import { Form as RemixForm })
is still the Remix component — match on import source, not local name.Complete Hard gates (especially gate 4), then report only issues that still pass the review-verification-protocol pre-report checks.