はじめに

Reactのフォーム送信は、見た目より事故が起きやすい。
入力値を集めてAPIへ送るだけに見えても、レビューでは次のような状態を確認する必要がある。

  • 送信ボタンを連打したらどうなるか
  • API応答待ちの間に入力値を変えたらどうなるか
  • バリデーション失敗と通信失敗を分けて扱っているか
  • 送信成功後に同じpayloadを再送できてしまわないか

この記事では、Reactフォームの「送信責務」をレビューする観点を整理する。
ポイントは、ボタンをdisabledにすることだけではない。どの単位を送信中として扱い、どこで再送を止めるかである。

まず止めたい実装

次のコードは、送信時にAPIを呼び、成功したら完了メッセージを出している。

二重送信を止めていない例
export function ProfileForm() {
  const [name, setName] = useState("");
  const [message, setMessage] = useState("");

  async function handleSubmit(event: React.FormEvent) {
    event.preventDefault();

    const response = await fetch("/api/profile", {
      method: "POST",
      headers: { "Content-Type": "application/json" },
      body: JSON.stringify({ name }),
    });

    if (response.ok) {
      setMessage("保存しました");
    }
@Reviewer
送信中状態がないため、連打やEnter連続入力で同じ内容が複数回送信されます。サーバー側の冪等性に依存するなら、その前提を明示してください。
} return ( <form onSubmit={handleSubmit}> <input value={name} onChange={event => setName(event.target.value)} /> <button type="submit">保存</button> {message && <p>{message}</p>} </form> ); }

この実装はデモなら成立する。
しかし実務では、同じ保存リクエストが複数回走る可能性がある。

プロフィール更新ならまだ影響は小さいかもしれない。
注文作成、チケット発行、招待メール送信、ポイント利用のような処理では、そのまま障害になる。

レビューで見るべき本質

フォーム送信のレビューでは、UIイベントではなく業務上の実行単位を見る。

次の問いに答えられない実装は危ない。

  • 1回の送信とは何を指すのか
  • 送信中に同じ操作を受け付けるのか
  • 失敗時に再送してよいのか
  • 成功後にフォームを残すのか、閉じるのか
  • サーバー側に冪等キーがあるのか

isSubmitting を入れるだけでは不十分なことがある。
ただし、まずは送信中状態が明示されているかが第一の確認線になる。

レビュー観点1:pending状態が同期的に立つか

送信中フラグは、API呼び出し前に立てる必要がある。

pendingを持つ例
export function ProfileForm() {
  const [name, setName] = useState("");
  const [isSubmitting, setIsSubmitting] = useState(false);
  const [errorMessage, setErrorMessage] = useState("");

  async function handleSubmit(event: React.FormEvent) {
    event.preventDefault();

    if (isSubmitting) {
      return;
    }

    setIsSubmitting(true);
    setErrorMessage("");

    try {
      const response = await fetch("/api/profile", {
        method: "POST",
        headers: { "Content-Type": "application/json" },
        body: JSON.stringify({ name }),
      });

      if (!response.ok) {
        throw new Error("failed to save profile");
      }
    } catch {
      setErrorMessage("保存できませんでした");
    } finally {
      setIsSubmitting(false);
    }
  }

  return (
    <form onSubmit={handleSubmit}>
      <input
        value={name}
        onChange={event => setName(event.target.value)}
        disabled={isSubmitting}
      />
      <button type="submit" disabled={isSubmitting}>
        保存
      </button>
      {errorMessage && <p role="alert">{errorMessage}</p>}
    </form>
  );
}

ここでレビューしたいのは、disabled の有無だけではない。
isSubmitting がAPI前に立ち、成功・失敗の両方で解除されることを確認する。

Comment
@Reviewer: 送信中状態はありますが、API呼び出し後に設定されているため、短時間の連打を防げません。送信処理の入口で同期的にpendingへ遷移させてください。

レビュー観点2:入力値のスナップショットを送っているか

送信中に入力変更を許す場合、送信した値と画面上の値がずれる。

待機中に値が変わる例
async function handleSubmit(event: React.FormEvent) {
  event.preventDefault();
  setIsSubmitting(true);

  await saveProfile({ name });

  setIsSubmitting(false);
}

このコードでは、await 中に name が変わる可能性がある。
JavaScriptのクロージャとして送信payloadは呼び出し時点の値になるが、ユーザーから見ると「今画面にある値が保存された」と誤解しやすい。

レビューでは方針を明確にする。

方針 確認すること
送信中は入力を止める inputをdisabledまたはreadOnlyにする
入力変更を許す 送信中の値と現在値の差をUIで破綻させない
自動保存にする 送信単位とキャンセル単位を別途設計する

明示されていない場合は、送信中に入力を止める方がレビューしやすい。

レビュー観点3:クライアントの二重送信対策だけに依存していないか

React側でボタンをdisabledにしても、二重実行を完全には防げない。

  • ネットワーク再送
  • ブラウザの戻る・再実行
  • 別タブからの同時送信
  • APIクライアントや自動テストからの直接実行

業務上二重登録が致命的なら、サーバー側の冪等性が必要になる。

冪等キーを送る例
const requestId = crypto.randomUUID();

await fetch("/api/orders", {
  method: "POST",
  headers: {
    "Content-Type": "application/json",
    "Idempotency-Key": requestId,
  },
  body: JSON.stringify({ itemId, quantity }),
});

フロントエンドレビューでも、ここは指摘対象にしてよい。
UIで連打を止める責務と、業務上の重複作成を防ぐ責務は別だからだ。

Comment
@Reviewer: UI上のdisabledだけでは二重作成の保証にはなりません。注文作成のように重複が業務影響を持つ処理なので、API側の冪等キーまたは重複判定の有無を確認したいです。

レビュー観点4:バリデーション失敗と通信失敗を混ぜていないか

送信失敗をすべて同じメッセージにすると、ユーザーも実装者も次の行動を判断できない。

失敗理由が混ざる例
if (!response.ok) {
  setErrorMessage("保存できませんでした");
}

レビューでは少なくとも次を分けたい。

  • 入力値が不正
  • 認証・認可が切れている
  • 競合更新が起きた
  • 通信やサーバー障害で失敗した

これらはUIの戻し方が違う。
入力値不正ならフォームに残す。認可エラーなら操作自体を隠すか無効化する。競合更新なら再読み込みや差分確認が必要になる。

改善の方向性

レビューしやすいフォーム送信は、次のように状態遷移が読める。

送信責務を読みやすくした例
async function submitProfile() {
  if (submission.status === "submitting") {
    return;
  }

  const payload = buildProfilePayload(formValues);
  const validationError = validateProfile(payload);
  if (validationError) {
    setSubmission({ status: "invalid", error: validationError });
    return;
  }

  setSubmission({ status: "submitting" });

  try {
    const savedProfile = await saveProfile(payload);
    setProfile(savedProfile);
    setSubmission({ status: "succeeded" });
  } catch (error) {
    setSubmission({ status: "failed", error: toSubmitError(error) });
  }
}

ここでは、送信前検証、送信中、成功、失敗が状態として分かれている。
レビューアーは制御フローを追いやすく、失敗時のUIも設計しやすい。

レビューコメント例

Comment
@Reviewer: `onSubmit` が多重に実行された場合の制御が見えません。API呼び出し前に送信中状態へ遷移し、同一フォームの再送を止める構造にしてください。
Comment
@Reviewer: この処理は登録系の副作用を持つため、フロントエンドのdisabledだけでは重複作成対策として弱いです。サーバー側の冪等キーや重複判定の前提を確認したいです。

まとめ

Reactフォームのレビューでは、送信ボタンの見た目よりも、送信責務の境界を見る。

  • pending状態がAPI前に立つか
  • 送信中の入力変更をどう扱うか
  • クライアント側だけで二重登録を防ごうとしていないか
  • 失敗理由ごとにUIの戻し方が分かれているか

フォーム送信は小さな実装に見える。
しかし副作用を持つ処理では、二重実行を止める設計こそレビューで確認すべき中心になる。