remix-v2-forms について
Remix v2 開発者向けのフォームとミューテーションのスキルです。action、フォーム、状態フックの連携を整理し、プログレッシブエンハンスメントとデータ再検証を支援します。
主な機能
- 01画面遷移と画面内送信の使い分け
- 02action でのフォームデータ処理
- 03エラーと処理中状態の参照
- 04楽観的 UI とファイルアップロード
- 05用途に応じた Remix API の選択
活用シーン
- 01作成後にレコード詳細へ遷移
- 02リスト項目の編集や状態切り替え
- 03全体または局所の処理中表示
- 04検証付きファイルアップロードの実装
インストール
コマンドをコピーしてターミナルで実行します。
npx skills add https:/ / github.com/ existential-birds/ beagle --skill remix-v2-formsインストール後、Claude Code で/remix-v2-formsを入力して読み込まれたか確認します。
同梱ファイル
SKILL.md と一緒にインストールされる参考資料やスクリプト。
references/fetcher.mdform.mdintent-actions.mdoptimistic-ui.mduploads.md詳細
Remix v2 のフォームとミューテーション
@remix-run/react@^2 のルートモジュールフレームワークで標準となるミューテーションの基本機能です。正しい Remix v2 のミューテーションは、<Form method="post">(または <fetcher.Form>)、request.formData() を解析して redirect(...) または json(...) を返す action、そしてエラーには useActionData()(または fetcher.data)、保留状態には useNavigation()(または fetcher.state)を参照する UI で構成されます。この一連の処理を迂回する方法(fetch()、通常の <form>、e.preventDefault() とクライアント状態の組み合わせ)では、再検証、プログレッシブエンハンスメント、競合に安全な遷移がひそかに失われます。
クイックリファレンス
<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>
);
}
基本機能
| 名前 | 目的 |
|---|---|
@remix-run/react の <Form> | ルートの action にデータを送信し、ページ全体の再検証を実行する、ナビゲーション可能でプログレッシブエンハンスメントに対応したフォーム |
<Form navigate={false}> | 「fetcher 経由で送信し、ナビゲーションしない」ための簡略表記です。fetcher の参照を保持しない <fetcher.Form> と同等で、プログラムから操作するためのハンドルではなく、保留状態だけが必要な場合に便利です |
useFetcher() | インラインのミューテーション、リスト行、ポップオーバーなどで使う、ナビゲーションを伴わない送信チャネルです。同じ再検証を行い、URL は変更されません |
useFetchers() | アプリ全体で進行中のすべての fetcher 状態を含む読み取り専用の配列です。props のバケツリレーなしで、グローバルな保留インジケーター(トップバーのローダーなど)に使用します。返される項目には Form/submit/load メソッドはなく、formData、state などだけが含まれます |
useNavigation() | ページレベルのナビゲーションを監視します。<Form> の保留状態を示す信頼できる情報源です |
useSubmit() | プログラムによる送信(onChange による自動保存、キーボードショートカットなど)に使用します。HTMLFormElement、FormData、通常のオブジェクト(フォームエンコード)、または { encType: "application/json" } を指定して JSON としてエンコードした通常のオブジェクトを受け取ります |
useActionData<typeof action>() | 現在のルートで直近に実行された action の結果を読み取ります |
状態遷移:
useNavigation().state:GET 以外のフォーム送信ではidle → submitting → loading → idle、GET ナビゲーションではidle → loading → idleです。useFetcher().state:idle → submitting → loading → idleです。
非対称性: GET ナビゲーションでは useNavigation は submitting を経由しません。useFetcher は経由します。例外は fetcher.load() のみで、こちらは経由しません。<fetcher.Form method='get'> と fetcher.submit(..., {method:'get'}) はどちらも submitting を経由します。
主なパターン
ナビゲーションには <Form>、その場での処理には useFetcher
<Form> は URL を変更して履歴に追加し、すべての loader を再検証します。
useFetcher も同じ再検証を行いますが、現在の URL にとどまります。
各 useFetcher() 呼び出しは独立した送信チャネルを返すため、
2 つの行が同時に送信されても、保留状態は共有されません。
1 つのルートで複数のアクションを扱う intent パターン
1 つの action で formData.get("intent") を切り替え、操作ごとに
<button name="intent" value="..."> を使います。クリックした送信ボタンの
name=value だけがリクエスト本文に含まれます。references/intent-actions.md を参照してください。
formData を使った楽観的 UI
fetcher.formData と navigation.formData は送信時に同期的に設定され、
idle になるとクリアされます。レンダーごとに直接読み取り、ローカルの
React 状態に複製しないでください。references/optimistic-ui.md を参照してください。
ファイルアップロードには encType="multipart/form-data" が必要です
これを指定しないと、request.formData() はファイルデータを取り除き、
File ではなくファイル名の文字列を返します。
unstable_parseMultipartFormData と、サイズ上限を設定したアップロードハンドラーで
解析してください。unstable_ のプレフィックスは v2 でも恒久的に付きます。
references/uploads.md を参照してください。
ゲート(判断の順序)
順番どおりに回答してください。Pass は条件が真であることを意味します。同じ行にある API を選び、そこで終了してください。
<Form> vs useFetcher
- ミューテーション後に URL を変更する必要がありますか(レコードを作成して
/records/:idに移動する、削除してリストに戻る、 複数ステップのフローなど)?- はい →
<Form method="post">と、action からのredirect(...)を使います。終了。 - いいえ → ステップ 2。
- はい →
- ユーザーが同じページにとどまったまま、行、セル、トグル、または
サブセクションに対してミューテーションを行いますか(お気に入り、いいね、
数量の増加、インライン編集など)?
- はい →
<fetcher.Form>を使うuseFetcher()を使います。終了。 - いいえ → ステップ 3。
- はい →
- 通常のナビゲーション以外でデータを読み込みますか(ポップオーバーの内容、
コンボボックスの結果、プリフェッチなど)?
- はい →
fetcher.load(href)を使います。終了。 - いいえ →
<Form>をデフォルトとして使います。ナビゲーションが最も堅実な 選択肢です。再検証と履歴が標準で機能します。
- はい →
厳守事項:アプリ内で独自の Remix ルートに対してミューテーションを行う際に、
fetch() や axios を使ってはいけません。これらを使うと action のライフサイクルが迂回され、
loader の再検証が行われません。
保留中の状態を示す useNavigation と useFetcher.state
- 保留中を示すインジケーターはグローバルですか(root のページスピナー、上部の
読み込みバーなど)?
- はい →
root.tsxでuseNavigation()を使います (navigation.state !== "idle")。終了。 - いいえ → ステップ 2。
- はい →
- ミューテーションは
useFetcherで実行しましたか?- はい → その fetcher の
fetcher.stateを使います。useNavigation()は fetcher の処理状況を反映しません。終了。 - いいえ → ステップ 3。
- はい → その fetcher の
- リスト内の 1 つの行やボタンに限定されたインジケーターで、各行に
独自の fetcher がありますか?
- はい → 行ごとの
fetcher.stateを使います(またはuseFetchers()でキーを指定して 取得します)。これにより、ほかの行がちらつくのを防げます。終了。 - いいえ → ステップ 4。
- はい → 行ごとの
- インジケーターは
<Form>で送信したフォームに限定されますか?- はい →
useNavigation()を使い、navigation.formAction === "/expected-path"も確認して、無関係なナビゲーションで ローカルのスピナーが起動しないようにします。終了。 - いいえ → ステップ 5。
- はい →
- 楽観的な値をレンダリングする必要がありますか?
- はい →
navigation.formData?.get("field")(ページのフォーム)またはfetcher.formData?.get("field")(fetcher)を読み取ります。どちらもstate !== "idle"の間は値が設定されています。終了。
- はい →
追加ドキュメント
<Form>コンポーネント:<Form>とネイティブの<form>とfetch()の違い、 プログレッシブエンハンスメント、成功後のリダイレクト、useActionDataによる バリデーションエラーの表示については、references/form.md を参照してください。useFetcher:インラインミューテーション、リスト操作、ポップオーバー、fetcher.state、fetcher.data、fetcher.Form、fetcher.submit、fetcher.loadについては、 references/fetcher.md を参照してください。- 楽観的 UI:
fetcher.formDataとuseNavigation.formData、適用するタイミング、 失敗時の取り消しについては、 references/optimistic-ui.md を参照してください。 - ファイルのアップロード:
unstable_parseMultipartFormData、unstable_createMemoryUploadHandler、unstable_createFileUploadHandler、および上限付きハンドラーについては、 references/uploads.md を参照してください。 - インテントベースのアクション:FormData の
intentフィールドを使って 1 つのルートで 複数のアクションを実行する方法については、 references/intent-actions.md を参照してください。
比較
| 懸念事項 | <Form> | useFetcher | ネイティブの <form> | fetch() |
|---|---|---|---|---|
| URL の変更/履歴エントリー | はい | いいえ | はい(ハードナビゲーション) | いいえ |
| JS なしで動作 | はい | はい | はい | いいえ |
| loader を再検証 | はい | はい | はい(ハードリロード) | いいえ |
| 保留中の状態を取得するフック | useNavigation() | fetcher.state | なし | 手動 |
| 楽観的入力の取得元 | navigation.formData | fetcher.formData | なし | 手動 |
| アプリ内ミューテーションの用途 | 作成/削除/複数ステップ | インライン/行/トグル | 外部ターゲットのみ | 独自のルートには使用しない |