【JavaScript】フォームの二重送信を防ぐ方法

送信ボタンの連打や、戻る→再送信などで同じフォームが二重に送信されると、データの重複登録やサーバーの無駄な負荷につながります。これはJavaScriptとサーバー側の両面で防ぎます。

ポイントは多層で防ぐことです。①ボタン無効化(UI)、②フラグで送信中をガード(ロジック)、③サーバー側のワンタイムトークン(最後の砦)を組み合わせます。この記事ではそれぞれを動くコードで解説します。

この記事の結論:クライアント側は送信ボタンの disabledisSubmitting フラグでガードします。ただしクライアントの対策は迂回できるため、確実な防止はサーバー側のワンタイムトークン(idempotencyキー)で行います。CSRFトークンは二重送信防止ではない点に注意してください。
スポンサーリンク

送信ボタンを無効化する(UI側の基本)

最も手軽なのは、送信時に送信ボタンを disabled にして連打を防ぐ方法です。インラインの onsubmit 属性ではなく、addEventListener でまとめます。

HTML
<form id="form" action="/submit">
  <input type="text" name="name" required>
  <button type="submit">送信</button>
</form>
JavaScript:送信時にボタンを無効化
const form = document.getElementById("form");

form.addEventListener("submit", () => {
  const btn = form.querySelector('button[type="submit"]');
  btn.disabled = true; // 送信後は押せなくなる(通常送信はそのまま進む)
});
disabledの注意:無効化したボタンはname/valueが送信データに含まれません<button name="action" value="save"> のようにボタンの値をサーバーで使っている場合は、disabledにすると値が届かなくなります。その場合は次のフラグ方式や、別の隠しフィールドで状態を送ってください。

フラグで送信中をガードする(ロジック側)

ボタンの無効化だけだと、Enterキーやスクリプトからの送信をすり抜ける可能性があります。isSubmitting フラグで送信中かどうかを管理し、2回目以降を event.preventDefault() で止めると確実です。Enterキーによる送信そのものを制御したい場合はEnterキーでフォーム送信を無効化する方法も参考になります。

JavaScript:isSubmittingフラグでガード
let isSubmitting = false;

form.addEventListener("submit", (event) => {
  if (isSubmitting) {
    event.preventDefault(); // 2回目以降の送信を止める
    return;
  }
  isSubmitting = true;
});

通常のページ遷移を伴う送信ではページが切り替わるためフラグは自然にリセットされますが、次の非同期送信(fetch)では送信完了後にフラグを戻す処理が重要になります。

fetchでの非同期送信と二重送信防止

ページ遷移せずに送信する場合は fetch を使います。送信中はフラグとボタンでガードし、完了時に finally で必ず解除するのが定石です。古い XMLHttpRequest ではなく fetchasync/await で書きます。

JavaScript:fetch+try/finallyで確実に制御
const btn = form.querySelector('button[type="submit"]');
let isSubmitting = false;

form.addEventListener("submit", async (event) => {
  event.preventDefault();      // 通常送信をキャンセル
  if (isSubmitting) return;    // 送信中なら無視

  isSubmitting = true;
  btn.disabled = true;

  try {
    const res = await fetch(form.action, {
      method: "POST",
      body: new FormData(form),
    });
    if (!res.ok) throw new Error(`HTTP ${res.status}`);
    // 成功時の処理(例:完了画面へ遷移など)
  } catch (err) {
    console.error("送信に失敗しました", err);
  } finally {
    isSubmitting = false;      // 成否に関わらず必ず解除
    btn.disabled = false;
  }
});

finally で解除することで、エラー時に送信できなくなる事故を防げます(成功時はそのまま完了画面へ遷移し、再有効化しない設計も有効です)。fetch の基本はfetch APIの使い方、フォームデータの送信はFormDataの使い方、送信中のローディング表示はフォーム送信のローディング実装を参考にしてください。

サーバー側が本命:ワンタイムトークン(idempotency)

クライアント側の対策は開発者ツールなどで簡単に迂回できます。ネットワークの遅延でリクエストが二重に届くこともあります。そのため確実な二重送信防止はサーバー側で行います。

定番はワンタイムトークンです。フォーム表示時に一意のトークンを発行してサーバーに保存し、送信を受け取ったらそのトークンを使い捨て(消費)します。同じトークンで2回目が来たら「処理済み」として弾く仕組みです。APIならidempotencyキー、画面遷移ならPRG(Post/Redirect/Get)パターンと組み合わせます。

CSRFトークンとの違い:CSRFトークンなりすまし(別サイトからの不正送信)対策で、通常はセッション中何度でも再利用できるため、それ自体では二重送信を防げません。二重送信を防ぐには、一度使ったら無効になるワンタイムトークンが必要です。両者は目的が異なるので混同しないようにしましょう。

よくある質問(FAQ)

QJavaScriptでフォームの二重送信を防ぐには?
Asubmit イベントで送信ボタンを disabled にするのが基本です。さらに isSubmitting のようなフラグ変数で送信中を管理し、2回目以降を event.preventDefault() でキャンセルすると確実です。
Q非同期送信(fetch)での二重送信防止は?
AisSubmitting フラグを boolean で管理し、送信中は true にして再送信を防ぎ、完了後に finallyfalse に戻します。ボタンの disabled も併用すると、UIとロジックの両面で防げます。
QCSRFトークンがあれば二重送信は防げますか?
Aいいえ。CSRFトークンはなりすまし対策で、二重送信防止とは別物です。通常CSRFトークンはセッション中再利用できるため、同じトークンで2回送信しても両方通ります。二重送信を防ぐには、一度で無効化されるワンタイムトークンを使います。
Q二重送信対策はサーバー側でも必要ですか?
Aはい。クライアント側の対策は迂回できるため、サーバー側でもidempotencyキーやワンタイムトークン、トランザクション制御で二重処理を防いでください。最終的な防御はサーバー側が担います。

まとめ

フォームの二重送信は、①ボタンの disabled(UI)→ ②isSubmitting フラグ+preventDefault(ロジック)→ ③サーバー側のワンタイムトークン(最後の砦)という多層で防ぐのが基本です。

非同期送信では fetchtry/finally成否に関わらず確実に状態を戻すのがポイントです。そしてCSRFトークンは二重送信防止ではないことを押さえ、確実な防止はサーバー側のワンタイムトークン(idempotencyキー)で行いましょう。