htmx 逆引きレシピ
保存の二重送信を防ぐには?

公開日:
最終更新日:

保存ボタンの連打や通信遅延があると、同じデータが二重に登録される事故が起きがちです。
htmxなら、送信中の無効化・ローディング表示・リクエスト競合の制御までを、属性だけでまとめて実装できます。

このページでは「保存ボタン連打を防止」「通信中はフォームを無効化」「競合を避けたい」を1つのデモに統合し、hx-disabled-elt / hx-indicator / hx-sync / hx-trigger を使った“安全な保存”の定番パターンを解説します。

使用するhtmx属性

  • hx-trigger:リクエストを発火するタイミングを指定(このデモは submit で送信)
  • hx-indicator:通信中表示(保存中...)を出す要素を指定し、ユーザーに待ち状態を伝える
  • hx-disabled-elt:通信中だけボタンやフォーム要素を disabled にして、連打や編集を防止する
  • hx-sync:同じ要素からのリクエストが重なった時の挙動を制御し、競合を回避する(例:this:drop

利用シーン

  • 「保存ボタン連打を防止」:通信が遅い環境でも二重登録を起こしたくない
  • 「通信中はフォームを無効化」:送信中に入力が変わる/同時編集される事故を避けたい
  • 「競合を避けたい」:同じフォームから複数リクエストが飛んだ時に、後続を落とす/置き換えるなど制御したい

保存の二重送信を防ぐには?

保存ボタンの連打や通信遅延による二重送信を、htmxの属性だけで防ぎます。
通信中はフォームを無効化し、さらにリクエスト競合(同時送信)も抑止します。

HTML

<div class="DEMO">

	<h4>保存の二重送信を防ぐには?</h4>

	<form
		id="DEMO_DOUBLE_SUBMIT_FORM"
		class="FORM"
		method="post"
		hx-post="/htmx/demo/_double_submit_save.php"
		hx-trigger="submit"
		hx-target="#DEMO_DOUBLE_SUBMIT_RESULT"
		hx-swap="innerHTML"
		hx-indicator="#LOADER_MODAL"
		hx-disabled-elt="#DEMO_DOUBLE_SUBMIT_SAVE, #DEMO_DOUBLE_SUBMIT_FORM input, #DEMO_DOUBLE_SUBMIT_FORM select, #DEMO_DOUBLE_SUBMIT_FORM textarea"
		hx-sync="this:drop"
	>

		<label>
			タイトル
			<input type="text" name="title" maxlength="64" value="申請:端末貸与">
		</label>

		<label>
			担当
			<select name="owner">
				<option value="tanaka">tanaka</option>
				<option value="sato">sato</option>
				<option value="suzuki" selected>suzuki</option>
			</select>
		</label>

		<label>
			メモ
			<textarea name="memo" rows="3" maxlength="256">急ぎです。</textarea>
		</label>

		<button id="DEMO_DOUBLE_SUBMIT_SAVE" type="submit" class="BTN is-ok">
			保存
		</button>

		<div id="LOADER_MODAL" aria-hidden="true">
			<div id="LOADER" role="status" aria-live="polite" aria-label="通信中"></div>
		</div>

	</form>

	<div id="DEMO_DOUBLE_SUBMIT_RESULT" class="FORM-RESULT">
		<span class="HTMX-NOTE">「保存」を連打しても、通信中は無効化され、競合も抑止されます。</span>
	</div>

</div>

PHP

<?php
// 型を厳密に扱う
declare(strict_types=1);

// 返すのはHTML(UTF-8)
header('Content-Type: text/html; charset=UTF-8');

// HTMLエスケープ関数
function h(string $s): string {
	// 特殊文字をエスケープする
	return htmlspecialchars($s, ENT_QUOTES, 'UTF-8');
}

// 体感できるように少し遅延させる(デモ用)
usleep(900000);

// リクエストメソッドを取得する
$method = (string)($_SERVER['REQUEST_METHOD'] ?? '');

// POST以外は弾く
if ($method !== 'POST') {

	// エラー表示を返す
	echo '<div class="FORM-RESULT is-ng"><strong>NG</strong>:POSTで送ってください</div>';

	// 終了する
	exit;
}

// titleを受け取る
$title = (string)($_POST['title'] ?? '');

// ownerを受け取る
$owner = (string)($_POST['owner'] ?? '');

// memoを受け取る
$memo = (string)($_POST['memo'] ?? '');

// 前後空白を除去する
$title = trim($title);

// 前後空白を除去する
$owner = trim($owner);

// 前後空白を除去する
$memo = trim($memo);

// titleが空なら補う(デモ用)
if ($title === '') $title = '(未入力)';

// ownerが空なら補う(デモ用)
if ($owner === '') $owner = '(未選択)';

// 保存時刻(デモ用)
$now = date('Y-m-d H:i:s');

// 成功結果を返す
echo '<div class="FORM-RESULT is-ok">';

// 見出しを返す
echo '<strong>保存しました。</strong>';

// 内容を返す
echo '<div>タイトル:' . h($title) . '</div>';

// 内容を返す
echo '<div>担当:' . h($owner) . '</div>';

// 内容を返す
echo '<div>メモ:' . h($memo) . '</div>';

// 保存時刻を返す
echo '<div class="HTMX-NOTE">保存時刻:' . h($now) . '</div>';

// 閉じる
echo '</div>';

CSS

#LOADER_MODAL{
	display: none;
	position: fixed;
	inset: 0;
	background: rgba(0,0,0,0.35);
	z-index: 9999;
}

#LOADER_MODAL.htmx-request{
	display: block;
}

#LOADER{
	display: flex;
	align-items: center;
	justify-content: center;
	position: absolute;
	top: 50%;
	left: 50%;
	transform: translate(-50%, -50%);
	border: none;
	box-shadow: none;
	background-color: transparent;
	background-image: url('data:image/svg+xml;charset=utf8,%3Csvg%20xmlns%3D%22http%3A%2F%2Fwww.w3.org%2F2000%2Fsvg%22%20viewBox%3D%220%200%2024%2024%22%20width%3D%2248%22%20height%3D%2248%22%20fill%3D%22%23ffffff%22%3E%20%3Ccircle%20cx%3D%2212%22%20cy%3D%2212%22%20r%3D%220%22%3E%20%3Canimate%20attributeName%3D%22opacity%22%20values%3D%220%3B1%3B0%22%20keyTimes%3D%220%3B.05%3B1%22%20calcMode%3D%22linear%22%20dur%3D%221s%22%20repeatCount%3D%22indefinite%22%2F%3E%20%3Canimate%20attributeName%3D%22r%22%20from%3D%220%22%20to%3D%2212%22%20dur%3D%221s%22%20repeatCount%3D%22indefinite%22%2F%3E%20%3C%2Fcircle%3E%20%3Ccircle%20cx%3D%2212%22%20cy%3D%2212%22%20r%3D%220%22%3E%20%3Canimate%20attributeName%3D%22opacity%22%20values%3D%220%3B1%3B0%22%20keyTimes%3D%220%3B.05%3B1%22%20calcMode%3D%22linear%22%20dur%3D%221s%22%20begin%3D%22.3s%22%20repeatCount%3D%22indefinite%22%2F%3E%20%3Canimate%20attributeName%3D%22r%22%20from%3D%220%22%20to%3D%2212%22%20dur%3D%221s%22%20begin%3D%22.3s%22%20repeatCount%3D%22indefinite%22%2F%3E%20%3C%2Fcircle%3E%20%3Ccircle%20cx%3D%2212%22%20cy%3D%2212%22%20r%3D%220%22%3E%20%3Canimate%20attributeName%3D%22opacity%22%20values%3D%220%3B1%3B0%22%20keyTimes%3D%220%3B.05%3B1%22%20calcMode%3D%22linear%22%20dur%3D%221s%22%20begin%3D%22.6s%22%20repeatCount%3D%22indefinite%22%2F%3E%20%3Canimate%20attributeName%3D%22r%22%20from%3D%220%22%20to%3D%2212%22%20dur%3D%221s%22%20begin%3D%22.6s%22%20repeatCount%3D%22indefinite%22%2F%3E%20%3C%2Fcircle%3E%3C%2Fsvg%3E');
	background-size: 200px 200px;
	background-position: center center;
	background-repeat: no-repeat;
}

#LOADER::after{
	content: '';
	height: 250px;
	width: 250px;
}

#LOADER_MODAL{
	pointer-events: auto;
}

デモ

保存の二重送信を防ぐには?

「保存」を連打しても、通信中は無効化され、競合も抑止されます。

解説

1) 送信中の連打を防ぐ(保存ボタンの無効化)

  • フォームに hx-disabled-elt を指定し、送信中だけ保存ボタンを disabled にします。
  • これにより「保存を連打して二重送信」が起きにくくなります(UI側の第一防衛線)。

2) 送信中はフォーム全体を無効化(入力の変更を封じる)

  • hx-disabled-elt にフォーム内の input/select/textarea も含め、送信中の入力変更を防ぎます。
  • 「送信したつもりの内容」と「画面に残っている内容」がズレる混乱も減らせます。

3) リクエスト競合を抑止(同時送信を制御)

  • hx-sync="this:drop" を付け、同じフォームから送信が重なった場合は後続リクエストを捨てます。
  • 通信遅延や連打が発生しても、サーバー側で同時処理が増えにくく、競合や不整合を避けやすくなります。

4) ユーザーに「待ち」を見せる(ローディング表示)

  • hx-indicator で「保存中...」を表示し、クリックが効いていることを伝えます。
  • 待ち状態が見えると、連打そのものが起きにくくなります(体験面の予防)。

注意:UI側の対策(無効化/競合制御)は強力ですが、実務ではサーバー側でも「重複登録防止(トークン/一意制約)」を併用するとさらに安全です。

このページの著者

もちもちみかん(システムエンジニア)

社内SEとしてグループ企業向けの業務アプリを要件定義〜運用まで一気通貫で担当しています。

経験:Webアプリ/業務システム

得意:PHP・JavaScript・MySQL・CSS

個人実績:フォーム生成基盤クイズ学習プラットフォーム

詳しいプロフィールはこちら!  もちもちみかんのプロフィール

もちもちみかん0系くん
TOPへ

もちもちみかん.comとは


このサイトでは、コーディングがめんどうくさい人向けのお助けツールとして、フォームやCSSをノーコードで生成できる、
 もちもちみかん.forms
 もちもちみかん.css1
 もちもちみかん.css2
と言ったジェネレーターを用意してます。

また、このサイトを通じて、「もちもちみかん」のかわいさを普及したいとかんがえてます!