--- name: remix-v2-forms description: Remix v2 form submissions and mutations. Use when implementing forms, optimistic UI, file uploads, or multi-action routes. Triggers on
, useFetcher, useSubmit, useNavigation for pending state, unstable_parseMultipartFormData, fetcher.formData, intent-based actions, encType multipart. --- # Remix v2 Forms & Mutations Canonical mutation primitives for the `@remix-run/react@^2` route-module framework. A correct Remix v2 mutation is: a `` (or ``), 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 ``, `e.preventDefault()` + client state — silently sacrifices revalidation, progressive enhancement, and race-safe transitions. ## Quick Reference **`` + action**: ```tsx 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(); const nav = useNavigation(); const busy = nav.state !== "idle" && nav.formAction === "/signup"; return ( {actionData?.errors?.email ? {actionData.errors.email} : null} ); } ``` ## Primitives | Name | Purpose | |---|---| | `
` from `@remix-run/react` | Navigating, progressively-enhanced form that posts to a route `action` and triggers full-page revalidation | | `` | Shorthand for "post via fetcher; do not navigate." Equivalent to `` 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 `` 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()` | Read the most recent action result for the current route | State transitions: - `useNavigation().state`: `idle → submitting → loading → idle` for non-GET form 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. `` and `fetcher.submit(..., {method:'get'})` both transition through `submitting`. ## Key Patterns ### `` for navigation, `useFetcher` for in-place `` 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. ### Intent pattern for multiple actions on one route One `action`, switch on `formData.get("intent")`, distinct `