remix-v2-forms — independently scanned and version-tracked by SaferSkills.
SaferSkills independently audited remix-v2-forms (Agent Skill) and scored it 100/100 (green). The audit ran 55 deterministic rules across Security, Supply Chain, Maintenance, Transparency, and Community; it found 0 high-severity and 0 lower-severity findings. The full rule-by-rule trace and per-finding evidence are below. Free, methodology-open.
Findings & checks · 0 flagged
Every scanned point with the score it earned and what moved between them.
First recorded scan — no prior version to compare against.
The primary manifest — the file an agent reads to learn what this artifact does.
Canonical mutation primitives for the @remix-run/react@^2 route-module framework. A correct Remix v2 mutation is: a <Form method="post"> (or <fetcher.Form>), an action that parses request.formData() and returns either redirect(...) or json(...), and UI that reads useActionData() (or fetcher.data) for errors plus useNavigation() (or fetcher.state) for pending state. Anything that bypasses this loop — fetch(), raw <form>, e.preventDefault() + client state — silently sacrifices revalidation, progressive enhancement, and race-safe transitions.
`<Form>` + action:
import { json, redirect, type ActionFunctionArgs } from "@remix-run/node";
import { Form, useActionData, useNavigation } from "@remix-run/react";
export async function action({ request }: ActionFunctionArgs) {
const form = await request.formData();
const email = String(form.get("email") ?? "");
if (!email.includes("@")) return json({ errors: { email: "Invalid" } }, { status: 400 });
await createUser({ email });
return redirect("/dashboard");
}
export default function Signup() {
const actionData = useActionData<typeof action>();
const nav = useNavigation();
const busy = nav.state !== "idle" && nav.formAction === "/signup";
return (
<Form method="post" replace>
<input name="email" type="email" />
{actionData?.errors?.email ? <em>{actionData.errors.email}</em> : null}
<button disabled={busy}>{busy ? "Signing up..." : "Sign Up"}</button>
</Form>
);
}| Name | Purpose |
|---|---|
<Form> from @remix-run/react | Navigating, progressively-enhanced form that posts to a route action and triggers full-page revalidation |
<Form navigate={false}> | Shorthand for "post via fetcher; do not navigate." Equivalent to <fetcher.Form> without holding a fetcher ref — useful when you only need pending state, not a programmatic handle |
useFetcher() | Non-navigating submission channel for inline mutations, list rows, popovers — same revalidation, no URL change |
useFetchers() | Read-only array of all in-flight fetcher states across the app. Use for global pending indicators (top-bar loader) without prop drilling. No Form/submit/load methods on the returned items — just formData, state, etc. |
useNavigation() | Observes page-level navigation; the source of truth for <Form> pending state |
useSubmit() | Programmatic submission (onChange autosave, keyboard shortcuts). Accepts HTMLFormElement, FormData, plain object (form-encoded), or plain object encoded as JSON via { encType: "application/json" } |
useActionData<typeof action>() | Read the most recent action result for the current route |
State transitions:
useNavigation().state: idle → submitting → loading → idle for non-GETform submissions; idle → loading → idle for GET navigation.
useFetcher().state: idle → submitting → loading → idle.Asymmetry: useNavigation skips submitting for GET navigations; useFetcher does NOT — only fetcher.load() skips it. <fetcher.Form method='get'> and fetcher.submit(..., {method:'get'}) both transition through submitting.
<Form> for navigation, useFetcher for in-place<Form> changes the URL, adds history, and revalidates all loaders. useFetcher does the same revalidation but stays on the current URL. Each useFetcher() call returns an independent submission channel, so two rows submitting at once do not share pending state.
One action, switch on formData.get("intent"), distinct <button name="intent" value="..."> per operation. Only the clicked submit button's name=value lands in the body. See references/intent-actions.md.
formDatafetcher.formData and navigation.formData are populated synchronously on submit and cleared at idle. Read directly each render; never mirror into local React state. See references/optimistic-ui.md.
encType="multipart/form-data"Without it, request.formData() strips file data and you get the filename string instead of a File. Parse with unstable_parseMultipartFormData and a bounded upload handler. The unstable_ prefix is permanent in v2. See references/uploads.md.
Answer in order. Pass means the condition is true; pick the API on the same line and stop.
<Form> vs useFetcherand routing to /records/:id, deleting and going back to a list, multi-step flow)?
<Form method="post"> + redirect(...) from the action. Stop.the user stays on the same page** (favorite, like, increment quantity, inline edit)?
useFetcher() with <fetcher.Form>. Stop.combobox results, prefetch)?
fetcher.load(href). Stop.<Form>. Navigation is the conservativechoice — revalidation and history work out of the box.
Hard rule: never reach for fetch() or axios for in-app mutations against your own Remix routes. That bypasses the action lifecycle and skips loader revalidation.
useNavigation vs useFetcher.state for pending stateloading bar)?
useNavigation() in root.tsx(navigation.state !== "idle"). Stop.
fetcher.state. useNavigation()will NOT reflect fetcher activity. Stop.
row has its own fetcher?**
fetcher.state (or look up by key viauseFetchers()) so other rows do not flicker. Stop.
useNavigation() AND checknavigation.formAction === "/expected-path" so unrelated navigations don't trigger your local spinner. Stop.
navigation.formData?.get("field") (page form) orfetcher.formData?.get("field") (fetcher) — both are populated while state !== "idle". Stop.
<Form> vs native <form> vs fetch(), progressive enhancement, redirect-after-success, and validation error display via useActionData.
inline mutations, list operations, popovers, fetcher.state, fetcher.data, fetcher.Form, fetcher.submit, fetcher.load.
references/optimistic-ui.md for fetcher.formData and useNavigation.formData, when to apply, and reverting on failure.
for unstable_parseMultipartFormData, unstable_createMemoryUploadHandler, unstable_createFileUploadHandler, and bounded handlers.
references/intent-actions.md for multiple actions on one route via the FormData intent field.
| Concern | <Form> | useFetcher | Native <form> | fetch() |
|---|---|---|---|---|
| URL change / history entry | Yes | No | Yes (hard nav) | No |
| Works without JS | Yes | Yes | Yes | No |
| Revalidates loaders | Yes | Yes | Yes (hard reload) | No |
| Pending state hook | useNavigation() | fetcher.state | None | Manual |
| Optimistic input source | navigation.formData | fetcher.formData | None | Manual |
| In-app mutation use case | Create / delete / multi-step | Inline / row / toggle | External targets only | Never for own routes |
~30 seconds. Free. No account. Every finding cites a rule and a line of evidence.