Skip to content
Development
Skill

/remix-v2-forms

Remix v2 form submissions and mutations. Use when implementing forms, optimistic UI, file uploads, or multi-action routes. Triggers on <Form>, useFetcher, useSubmit, useNavigation for pending state, unstable_parseMultipartFormData, fetcher.formData, intent-based actions, encType

From plugin
beagle
82139 skills2 commands
Install
$ npx -y skills add existential-birds/beagle --skill remix-v2-forms --agent claude-code

How it fires

How this skill gets triggered: by you, by Claude, or both.

  • Fires itselfAuto-invocation. Claude auto-loads it when your prompt matches the work.Auto-invocation is when the right skill fires by itself at the right moment, driven by a FLOW.md router and a hook, instead of you invoking it by name. It is the difference between a skill being installed and a skill actually getting used.Read the full definition →
  • You can call itInvoke it directly when you want it.
  • Slash command/remix-v2-forms

Context preview

The summary Claude sees to decide when to auto-load this skill.

Remix v2 form submissions and mutations. Use when implementing forms, optimistic UI, file uploads, or multi-action routes. Triggers on <Form>, useFetcher, useSubmit, useNavigation for pending state, unstable_parseMultipartFormData, fetcher.formData, intent-based actions, encType

SKILL.md

remix-v2-forms.SKILL.md
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 <Form>, 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 `<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.

Quick Reference

**`<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>
  );
}

Primitives

| 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-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. `<fetcher.Form method='get'>` and `fetcher.submit(..., {method:'get'})` both transition through `submitting`.

Key Patterns

`<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.

Intent pattern for multiple actions on one route

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](references/intent-actions.md).

Optimistic UI from `formData`

`fetcher.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](references/optimistic-ui.md).

File uploads need `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](references/uploads.md).

Gates (decision sequencing)

Answer **in order**. **Pass** means the condition is true; pick the API on the same line and **stop**.

`<Form>` vs `useFetcher`

1. **Does the URL need to change after the mutation** (creating a record and routing to `/records/:id`, deleting and going back to a list, multi-step flow)?

  • **Pass →** `<Form method="post">` + `redirect(...)` from the action. **Stop.**
  • **Fail →** Step 2.

2. **Is this a mutation against a row, cell, toggle, or sub-section while the user stays on the same page** (favorite, like, increment quantity, inline edit)?

  • **Pass →** `useFetcher()` with `<fetcher.Form>`. **Stop.**
  • **Fail →** Step 3.

3. **Is this loading data outside of normal navigation** (popover content, combobox results, prefetch)?

  • **Pass →** `fetcher.load(href)`. **Stop.**
  • **Fail →** Default to `<Form>`. Navigation is the conservative

choice — 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 pen

Read more
Ships withbeagle

Image: NASA, Public Domain. Source Beagle is an Agent Skills marketplace: framework-aware code review, documentation, testing, architectural analysis, and git workflows for any compatible coding agent.

Get the whole plugin

Other skills on beagle.