htmx 逆引きレシピ
連打・競合・順番事故を防ぐには?

公開日:
最終更新日:

keyup 連打・遅延差による上書き・同時更新衝突は、管理画面で起きやすい事故です。

このページでは hx-trigger / hx-sync / hx-request を使い、事故る例から防ぐ例までを3デモで確認します。

使用するhtmx属性

タグ:hx-trigger / hx-sync / hx-request

  • hx-triggerkeyup changed delay:500ms で送信頻度を制御し、連打時の無駄通信を抑えます。
  • hx-sync:同じ更新対象の競合を制御し、古いレスポンス上書きや多重リクエストを防ぎます。
  • hx-requesttimeout などを指定し、体感速度と失敗時の復帰性を改善します。

利用シーン

  • 検索の打鍵が多すぎる:入力のたびに通信すると重いので、打ち終わった“最後の1回”だけ投げて検索結果を更新したい
  • 古いレスポンスで上書きされたくない:連続操作で遅いレスポンスが後から返ってきても、最新の結果だけを画面に反映したい
  • 同時更新の衝突を避ける:フィルタ変更・ページングなど複数操作が同じ領域を更新する時、リクエストを直列化して表示の事故を防ぎたい

共通PHP(全文)

データ生成・HTMLエスケープ・断片レンダリングを _debounce_sync_data.php に集約しています。
出力時は共通関数経由で htmlspecialchars を適用します。

PHP(_debounce_sync_data.php)

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

// HTML特殊文字を安全化する
function debounce_sync_h(string $value): string
{
	// エスケープ済み文字列を返す
	return htmlspecialchars($value, ENT_QUOTES, 'UTF-8');
}

// 現在時刻をミリ秒付きで返す
function debounce_sync_now(): string
{
	// 時刻文字列を返す
	return date('H:i:s.v');
}

// リクエスト文字列を安全に取り出す
function debounce_sync_request_text(array $source, string $key, string $fallback): string
{
	// 文字列を取り出して前後空白を除去する
	$raw = trim((string)($source[$key] ?? ''));

	// 空文字なら既定値を返す
	if ($raw === '') {
		// 既定値を返す
		return $fallback;
	}

	// 入力値を返す
	return $raw;
}

// キーワード候補データを返す
function debounce_sync_search_items(): array
{
	// 候補配列を返す
	return [
		'apple pie',
		'apricot jam',
		'banana milk',
		'black tea',
		'chocolate mint',
		'cinnamon roll',
		'coffee jelly',
		'ginger ale',
		'grape soda',
		'green tea',
		'hot sandwich',
		'ice cream',
		'lemon tart',
		'mango pudding',
		'melon bread',
		'milk tea',
		'mint candy',
		'orange juice',
		'peach soda',
		'strawberry shake',
	];
}

// 検索結果を返す
function debounce_sync_search_hits(string $q): array
{
	// キーワードを小文字化する
	$q = mb_strtolower(trim($q));

	// 全候補を取得する
	$items = debounce_sync_search_items();

	// 空検索は先頭8件を返す
	if ($q === '') {
		// 先頭8件を返す
		return array_slice($items, 0, 8);
	}

	// ヒット配列を初期化する
	$hits = [];

	// 各候補を確認する
	foreach ($items as $item) {
		// 比較用文字列を作る
		$needle = mb_strtolower($item);

		// 部分一致なら採用する
		if (mb_stripos($needle, $q) !== false) {
			// ヒット配列へ追加する
			$hits[] = $item;
		}
	}

	// 最大8件に制限して返す
	return array_slice($hits, 0, 8);
}

// slow/fastの遅延時間を決める
function debounce_sync_slowfast_delay_us(string $q): int
{
	// 比較用文字列を小文字化する
	$qLower = mb_strtolower(trim($q));

	// slowを含む場合は遅延を長くする
	if (mb_stripos($qLower, 'slow') !== false) {
		// 遅い応答を返す
		return 1400000;
	}

	// fastを含む場合は遅延を短くする
	if (mb_stripos($qLower, 'fast') !== false) {
		// 速い応答を返す
		return 220000;
	}

	// それ以外はランダム遅延を返す
	return random_int(350000, 1100000);
}

// マルチ更新用のカテゴリを返す
function debounce_sync_multi_filters(): array
{
	// カテゴリ配列を返す
	return ['all', 'tea', 'bread', 'sweet'];
}

// マルチ更新用の元データを返す
function debounce_sync_multi_items(): array
{
	// 元データ配列を返す
	return [
		['id' => 1,  'name' => 'green tea set',      'cat' => 'tea'],
		['id' => 2,  'name' => 'black tea pot',      'cat' => 'tea'],
		['id' => 3,  'name' => 'milk bread',         'cat' => 'bread'],
		['id' => 4,  'name' => 'cinnamon bread',     'cat' => 'bread'],
		['id' => 5,  'name' => 'apple tart',         'cat' => 'sweet'],
		['id' => 6,  'name' => 'chocolate parfait',  'cat' => 'sweet'],
		['id' => 7,  'name' => 'earl grey pack',     'cat' => 'tea'],
		['id' => 8,  'name' => 'baguette',           'cat' => 'bread'],
		['id' => 9,  'name' => 'strawberry cake',    'cat' => 'sweet'],
		['id' => 10, 'name' => 'oolong tea bottle',  'cat' => 'tea'],
		['id' => 11, 'name' => 'melon pan',          'cat' => 'bread'],
		['id' => 12, 'name' => 'custard pudding',    'cat' => 'sweet'],
	];
}

// フィルタ後の一覧を返す
function debounce_sync_multi_filtered(string $filter): array
{
	// フィルタ候補を取得する
	$filters = debounce_sync_multi_filters();

	// 不正値はallへ補正する
	if (!in_array($filter, $filters, true)) {
		// allへ補正する
		$filter = 'all';
	}

	// 全件データを取得する
	$items = debounce_sync_multi_items();

	// allなら全件返す
	if ($filter === 'all') {
		// 全件を返す
		return $items;
	}

	// 抽出配列を初期化する
	$picked = [];

	// 各データを確認する
	foreach ($items as $row) {
		// カテゴリ一致時のみ採用する
		if ((string)($row['cat'] ?? '') === $filter) {
			// 結果に追加する
			$picked[] = $row;
		}
	}

	// 抽出結果を返す
	return $picked;
}

// ページング結果を返す
function debounce_sync_multi_page(array $items, int $page, int $perPage = 4): array
{
	// ページ番号を1以上へ補正する
	$page = max(1, $page);

	// 件数を1以上へ補正する
	$perPage = max(1, $perPage);

	// 取得開始位置を計算する
	$offset = ($page - 1) * $perPage;

	// 1ページ分を返す
	return array_slice($items, $offset, $perPage);
}

// 最終ページ番号を返す
function debounce_sync_multi_last_page(int $count, int $perPage = 4): int
{
	// 件数を0以上へ補正する
	$count = max(0, $count);

	// 件数を1以上へ補正する
	$perPage = max(1, $perPage);

	// 最終ページを返す
	return max(1, (int)ceil($count / $perPage));
}

PHP(_debounce_sync_api.php)

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

// HTML特殊文字を安全化する
function debounce_sync_h(string $value): string
{
	// エスケープ済み文字列を返す
	return htmlspecialchars($value, ENT_QUOTES, 'UTF-8');
}

// 現在時刻をミリ秒付きで返す
function debounce_sync_now(): string
{
	// 時刻文字列を返す
	return date('H:i:s.v');
}

// リクエスト文字列を安全に取り出す
function debounce_sync_request_text(array $source, string $key, string $fallback): string
{
	// 文字列を取り出して前後空白を除去する
	$raw = trim((string)($source[$key] ?? ''));

	// 空文字なら既定値を返す
	if ($raw === '') {
		// 既定値を返す
		return $fallback;
	}

	// 入力値を返す
	return $raw;
}

// キーワード候補データを返す
function debounce_sync_search_items(): array
{
	// 候補配列を返す
	return [
		'apple pie',
		'apricot jam',
		'banana milk',
		'black tea',
		'chocolate mint',
		'cinnamon roll',
		'coffee jelly',
		'ginger ale',
		'grape soda',
		'green tea',
		'hot sandwich',
		'ice cream',
		'lemon tart',
		'mango pudding',
		'melon bread',
		'milk tea',
		'mint candy',
		'orange juice',
		'peach soda',
		'strawberry shake',
	];
}

// 検索結果を返す
function debounce_sync_search_hits(string $q): array
{
	// キーワードを小文字化する
	$q = mb_strtolower(trim($q));

	// 全候補を取得する
	$items = debounce_sync_search_items();

	// 空検索は先頭8件を返す
	if ($q === '') {
		// 先頭8件を返す
		return array_slice($items, 0, 8);
	}

	// ヒット配列を初期化する
	$hits = [];

	// 各候補を確認する
	foreach ($items as $item) {
		// 比較用文字列を作る
		$needle = mb_strtolower($item);

		// 部分一致なら採用する
		if (mb_stripos($needle, $q) !== false) {
			// ヒット配列へ追加する
			$hits[] = $item;
		}
	}

	// 最大8件に制限して返す
	return array_slice($hits, 0, 8);
}

// slow/fastの遅延時間を決める
function debounce_sync_slowfast_delay_us(string $q): int
{
	// 比較用文字列を小文字化する
	$qLower = mb_strtolower(trim($q));

	// slowを含む場合は遅延を長くする
	if (mb_stripos($qLower, 'slow') !== false) {
		// 遅い応答を返す
		return 1400000;
	}

	// fastを含む場合は遅延を短くする
	if (mb_stripos($qLower, 'fast') !== false) {
		// 速い応答を返す
		return 220000;
	}

	// それ以外はランダム遅延を返す
	return random_int(350000, 1100000);
}

// マルチ更新用のカテゴリを返す
function debounce_sync_multi_filters(): array
{
	// カテゴリ配列を返す
	return ['all', 'tea', 'bread', 'sweet'];
}

// マルチ更新用の元データを返す
function debounce_sync_multi_items(): array
{
	// 元データ配列を返す
	return [
		['id' => 1,  'name' => 'green tea set',      'cat' => 'tea'],
		['id' => 2,  'name' => 'black tea pot',      'cat' => 'tea'],
		['id' => 3,  'name' => 'milk bread',         'cat' => 'bread'],
		['id' => 4,  'name' => 'cinnamon bread',     'cat' => 'bread'],
		['id' => 5,  'name' => 'apple tart',         'cat' => 'sweet'],
		['id' => 6,  'name' => 'chocolate parfait',  'cat' => 'sweet'],
		['id' => 7,  'name' => 'earl grey pack',     'cat' => 'tea'],
		['id' => 8,  'name' => 'baguette',           'cat' => 'bread'],
		['id' => 9,  'name' => 'strawberry cake',    'cat' => 'sweet'],
		['id' => 10, 'name' => 'oolong tea bottle',  'cat' => 'tea'],
		['id' => 11, 'name' => 'melon pan',          'cat' => 'bread'],
		['id' => 12, 'name' => 'custard pudding',    'cat' => 'sweet'],
	];
}

// フィルタ後の一覧を返す
function debounce_sync_multi_filtered(string $filter): array
{
	// フィルタ候補を取得する
	$filters = debounce_sync_multi_filters();

	// 不正値はallへ補正する
	if (!in_array($filter, $filters, true)) {
		// allへ補正する
		$filter = 'all';
	}

	// 全件データを取得する
	$items = debounce_sync_multi_items();

	// allなら全件返す
	if ($filter === 'all') {
		// 全件を返す
		return $items;
	}

	// 抽出配列を初期化する
	$picked = [];

	// 各データを確認する
	foreach ($items as $row) {
		// カテゴリ一致時のみ採用する
		if ((string)($row['cat'] ?? '') === $filter) {
			// 結果に追加する
			$picked[] = $row;
		}
	}

	// 抽出結果を返す
	return $picked;
}

// ページング結果を返す
function debounce_sync_multi_page(array $items, int $page, int $perPage = 4): array
{
	// ページ番号を1以上へ補正する
	$page = max(1, $page);

	// 件数を1以上へ補正する
	$perPage = max(1, $perPage);

	// 取得開始位置を計算する
	$offset = ($page - 1) * $perPage;

	// 1ページ分を返す
	return array_slice($items, $offset, $perPage);
}

// 最終ページ番号を返す
function debounce_sync_multi_last_page(int $count, int $perPage = 4): int
{
	// 件数を0以上へ補正する
	$count = max(0, $count);

	// 件数を1以上へ補正する
	$perPage = max(1, $perPage);

	// 最終ページを返す
	return max(1, (int)ceil($count / $perPage));
}

① 検索の打鍵が多すぎる → debounce で減らす

左が「事故る例(遅延なし)」、右が「防ぐ例(delay:500ms)」です。
返却には request # と時刻を含め、送信回数の差が見えるようにしています。

HTML(view全文)

_demo_htmx_1.php
<div class="DEMO">

	<h4>DEMO1: 検索の打鍵が多すぎる → debounce で減らす</h4>

	<p class="HTMX-NOTE">
		同じ検索APIに対して、左は遅延なし、右は <code>delay:500ms</code> を設定しています。
	</p>

	<div class="GRID" style="display:grid;grid-template-columns:repeat(auto-fit,minmax(280px,1fr));gap:1rem;">

		<!-- 事故る例を表示する -->
		<section class="CARD">

			<!-- 見出しを表示する -->
			<h5>事故る例: 遅延なし(送信が多すぎる)</h5>

			<!-- 補足を表示する -->
			<p class="HTMX-NOTE">キー入力ごとにリクエストが飛ぶため、request # が増えやすくなります。</p>

			<!-- フォームを表示する -->
			<form class="FORM" method="get">

				<!-- ラベルを表示する -->
				<label>
					<!-- ラベル文言を表示する -->
					検索キーワード

					<!-- 入力欄を表示する -->
					<input
						type="text"
						name="q"
						maxlength="64"
						placeholder="例: tea / bread / sweet"
						hx-get="/htmx/demo/_debounce_sync_api.php?mode=search&amp;variant=raw"
						hx-trigger="keyup changed"
						hx-target="#DEMO1_RESULT_RAW"
						hx-swap="innerHTML"
					>
				</label>
			</form>

			<!-- 結果領域を表示する -->
			<div id="DEMO1_RESULT_RAW" class="FORM-RESULT">
				<!-- 初期メッセージを表示する -->
				<p class="HTMX-NOTE">ここに遅延なしの検索結果が表示されます。</p>
			</div>
		</section>

		<!-- 防ぐ例を表示する -->
		<section class="CARD">

			<!-- 見出しを表示する -->
			<h5>防ぐ例: debounce(delay:500ms)</h5>

			<!-- 補足を表示する -->
			<p class="HTMX-NOTE">入力が落ち着くまで待って送信するため、無駄な通信を減らせます。</p>

			<!-- フォームを表示する -->
			<form class="FORM" method="get">

				<!-- ラベルを表示する -->
				<label>
					<!-- ラベル文言を表示する -->
					検索キーワード

					<!-- 入力欄を表示する -->
					<input
						type="text"
						name="q"
						maxlength="64"
						placeholder="例: tea / bread / sweet"
						hx-get="/htmx/demo/_debounce_sync_api.php?mode=search&amp;variant=debounce"
						hx-trigger="keyup changed delay:500ms"
						hx-target="#DEMO1_RESULT_DEBOUNCE"
						hx-swap="innerHTML"
					>
				</label>
			</form>

			<!-- 結果領域を表示する -->
			<div id="DEMO1_RESULT_DEBOUNCE" class="FORM-RESULT is-ok">
				<!-- 初期メッセージを表示する -->
				<p class="HTMX-NOTE">ここにdebounceありの検索結果が表示されます。</p>
			</div>
		</section>
	</div>

</div>

デモ

DEMO1: 検索の打鍵が多すぎる → debounce で減らす

同じ検索APIに対して、左は遅延なし、右は delay:500ms を設定しています。

事故る例: 遅延なし(送信が多すぎる)

キー入力ごとにリクエストが飛ぶため、request # が増えやすくなります。

ここに遅延なしの検索結果が表示されます。

防ぐ例: debounce(delay:500ms)

入力が落ち着くまで待って送信するため、無駄な通信を減らせます。

ここにdebounceありの検索結果が表示されます。

② 古いレスポンスで上書きされたくない → hx-sync で制御

同一デモ内のチェックボックスで「対策なし/あり」を切り替えます。
q によって遅延時間を変えて、順番事故を再現しています。

HTML(view全文)

_demo_htmx_2.php
<div class="DEMO">

	<h4>DEMO2: 古いレスポンスで上書きされたくない → hx-sync で制御</h4>

	<p class="HTMX-NOTE">
		チェックボックスで「対策なし/あり」を切り替え、遅いレスポンスが後着する事故を再現します。
	</p>

	<section class="CARD">

		<h5>対策なし/ありの切替(同一デモ)</h5>

		<label class="HTMX-NOTE" style="display:flex;align-items:center;gap:.5rem;">
			<input type="checkbox" id="DEMO2_USE_SYNC" checked>
			対策あり(hx-sync="closest form:abort")を使う
		</label>

		<p class="HTMX-NOTE">「遅い検索」→すぐ「速い検索」を押すと、対策なしでは古い結果で上書きされる場合があります。</p>

		<div id="DEMO2_UNSAFE_CARD" class="CARD" hidden>
			<h6>事故る例: 対策なし</h6>
			<form id="DEMO2_UNSAFE_FORM" class="FORM" method="get">
				<div style="display:flex;flex-wrap:wrap;gap:.5rem;">
					<button
						type="button"
						class="BTN"
						hx-get="/htmx/demo/_debounce_sync_api.php?mode=slowfast&amp;q=slow-order"
						hx-target="#DEMO2_RESULT"
						hx-swap="innerHTML"
					>
						遅い検索を送信
					</button>
					<button
						type="button"
						class="BTN BTN_SUB"
						hx-get="/htmx/demo/_debounce_sync_api.php?mode=slowfast&amp;q=fast-order"
						hx-target="#DEMO2_RESULT"
						hx-swap="innerHTML"
					>
						速い検索を送信
					</button>
				</div>
			</form>
		</div>

		<div id="DEMO2_SAFE_CARD" class="CARD">
			<h6>防ぐ例: 対策あり(abort)</h6>
			<form id="DEMO2_SAFE_FORM" class="FORM" method="get">
				<div style="display:flex;flex-wrap:wrap;gap:.5rem;">
					<button
						type="button"
						class="BTN"
						hx-get="/htmx/demo/_debounce_sync_api.php?mode=slowfast&amp;q=slow-order&amp;safe=1"
						hx-target="#DEMO2_RESULT"
						hx-swap="innerHTML"
						hx-sync="closest form:abort"
					>
						遅い検索を送信
					</button>
					<button
						type="button"
						class="BTN BTN_SUB"
						hx-get="/htmx/demo/_debounce_sync_api.php?mode=slowfast&amp;q=fast-order&amp;safe=1"
						hx-target="#DEMO2_RESULT"
						hx-swap="innerHTML"
						hx-sync="closest form:abort"
					>
						速い検索を送信
					</button>
				</div>
			</form>
		</div>

		<div id="DEMO2_RESULT" class="FORM-RESULT">
			<p class="HTMX-NOTE">ここに結果が表示されます。遅い→速いの順で押して差を確認してください。</p>
		</div>

	</section>

<script>
(function () {
	const toggle = document.getElementById('DEMO2_USE_SYNC');
	const unsafe = document.getElementById('DEMO2_UNSAFE_CARD');
	const safe = document.getElementById('DEMO2_SAFE_CARD');

	if (!toggle || !unsafe || !safe) return;

	const apply = function () {
		if (toggle.checked) {
			unsafe.hidden = true;
			safe.hidden = false;
		} else {
			unsafe.hidden = false;
			safe.hidden = true;
		}
	};

	toggle.addEventListener('change', apply);
	apply();
})();
</script>

</div>

デモ

DEMO2: 古いレスポンスで上書きされたくない → hx-sync で制御

チェックボックスで「対策なし/あり」を切り替え、遅いレスポンスが後着する事故を再現します。

対策なし/ありの切替(同一デモ)

「遅い検索」→すぐ「速い検索」を押すと、対策なしでは古い結果で上書きされる場合があります。

防ぐ例: 対策あり(abort)

ここに結果が表示されます。遅い→速いの順で押して差を確認してください。

③ 同時更新の衝突を避ける → hx-sync で同じ更新グループを定義

フィルタ変更とページングが同じ領域を更新するケースです。
防ぐ例では hx-synchx-request='{"timeout":4000}' を併用します。

HTML(view全文)

_demo_htmx_3.php
<div class="DEMO">

	<h4>DEMO3: 同時更新の衝突を避ける → hx-sync でグルーピング</h4>

	<p class="HTMX-NOTE">
		フィルタ変更とページングが同じ結果領域を更新するケースで、競合制御を比較します。
	</p>

	<section class="CARD">

		<h5>同じ領域を更新する複数トリガー(フィルタ + ページング)</h5>

		<label class="HTMX-NOTE" style="display:flex;align-items:center;gap:.5rem;">
			<input type="checkbox" id="DEMO3_USE_SYNC" checked>
			対策あり(hx-syncグループ + hx-request timeout)を使う
		</label>

		<p class="HTMX-NOTE">フィルタ変更とページング操作を連続で行うと、対策なしでは反映順が前後しやすくなります。</p>

		<div id="DEMO3_UNSAFE_CARD" class="CARD" hidden>
			<h6>事故る例: 対策なし</h6>
			<div class="FORM" style="display:grid;gap:.5rem;">
				<label>
					フィルタ
					<select
						name="filter"
						hx-get="/htmx/demo/_debounce_sync_api.php?mode=multi&amp;channel=unsafe&amp;action=filter"
						hx-trigger="change"
						hx-target="#DEMO3_RESULT"
						hx-swap="innerHTML"
					>
						<option value="all">all</option>
						<option value="tea">tea</option>
						<option value="bread">bread</option>
						<option value="sweet">sweet</option>
					</select>
				</label>
				<div style="display:flex;flex-wrap:wrap;gap:.5rem;">
					<button
						type="button"
						class="BTN BTN_SUB"
						hx-get="/htmx/demo/_debounce_sync_api.php?mode=multi&amp;channel=unsafe&amp;action=page&amp;dir=prev"
						hx-target="#DEMO3_RESULT"
						hx-swap="innerHTML"
					>
						前ページ
					</button>
					<button
						type="button"
						class="BTN"
						hx-get="/htmx/demo/_debounce_sync_api.php?mode=multi&amp;channel=unsafe&amp;action=page&amp;dir=next"
						hx-target="#DEMO3_RESULT"
						hx-swap="innerHTML"
					>
						次ページ
					</button>
					<button
						type="button"
						class="BTN BTN_SUB"
						hx-get="/htmx/demo/_debounce_sync_api.php?mode=multi&amp;channel=unsafe&amp;action=init"
						hx-target="#DEMO3_RESULT"
						hx-swap="innerHTML"
					>
						再読み込み
					</button>
				</div>
			</div>
		</div>

		<div id="DEMO3_SAFE_CARD" class="CARD">
			<h6>防ぐ例: 対策あり(同じ更新グループ)</h6>
			<div id="DEMO3_SYNC_GROUP" class="FORM" style="display:grid;gap:.5rem;">
				<label>
					フィルタ
					<select
						name="filter"
						hx-get="/htmx/demo/_debounce_sync_api.php?mode=multi&amp;channel=safe&amp;action=filter"
						hx-trigger="change"
						hx-target="#DEMO3_RESULT"
						hx-swap="innerHTML"
						hx-sync="#DEMO3_SYNC_GROUP:replace"
						hx-request='{"timeout":4000}'
					>
						<option value="all">all</option>
						<option value="tea">tea</option>
						<option value="bread">bread</option>
						<option value="sweet">sweet</option>
					</select>
				</label>
				<div style="display:flex;flex-wrap:wrap;gap:.5rem;">
					<button
						type="button"
						class="BTN BTN_SUB"
						hx-get="/htmx/demo/_debounce_sync_api.php?mode=multi&amp;channel=safe&amp;action=page&amp;dir=prev"
						hx-target="#DEMO3_RESULT"
						hx-swap="innerHTML"
						hx-sync="#DEMO3_SYNC_GROUP:replace"
						hx-request='{"timeout":4000}'
					>
						前ページ
					</button>
					<button
						type="button"
						class="BTN"
						hx-get="/htmx/demo/_debounce_sync_api.php?mode=multi&amp;channel=safe&amp;action=page&amp;dir=next"
						hx-target="#DEMO3_RESULT"
						hx-swap="innerHTML"
						hx-sync="#DEMO3_SYNC_GROUP:replace"
						hx-request='{"timeout":4000}'
					>
						次ページ
					</button>
					<button
						type="button"
						class="BTN BTN_SUB"
						hx-get="/htmx/demo/_debounce_sync_api.php?mode=multi&amp;channel=safe&amp;action=init"
						hx-target="#DEMO3_RESULT"
						hx-swap="innerHTML"
						hx-sync="#DEMO3_SYNC_GROUP:replace"
						hx-request='{"timeout":4000}'
					>
						再読み込み
					</button>
				</div>
			</div>
		</div>

		<div id="DEMO3_RESULT" class="FORM-RESULT is-ok">
			<p class="HTMX-NOTE">ここに一覧が表示されます。操作後に request # と時刻を確認してください。</p>
		</div>
	</section>

<script>
(function () {
	const toggle = document.getElementById('DEMO3_USE_SYNC');
	const unsafe = document.getElementById('DEMO3_UNSAFE_CARD');
	const safe = document.getElementById('DEMO3_SAFE_CARD');

	if (!toggle || !unsafe || !safe) return;

	const apply = function () {
		if (toggle.checked) {
			unsafe.hidden = true;
			safe.hidden = false;
		} else {
			unsafe.hidden = false;
			safe.hidden = true;
		}
	};

	toggle.addEventListener('change', apply);
	apply();
})();
</script>

</div>

デモ

DEMO3: 同時更新の衝突を避ける → hx-sync でグルーピング

フィルタ変更とページングが同じ結果領域を更新するケースで、競合制御を比較します。

同じ領域を更新する複数トリガー(フィルタ + ページング)

フィルタ変更とページング操作を連続で行うと、対策なしでは反映順が前後しやすくなります。

防ぐ例: 対策あり(同じ更新グループ)

ここに一覧が表示されます。操作後に request # と時刻を確認してください。

次に読むオススメレシピ

このサイトの運営者

もちもちみかん(社内SE)

運営者は、グループ企業向けの業務アプリを要件定義から運用まで担当している社内SEです。PHP・JavaScript・MySQL・CSSを使った実務経験をもとに、一次情報や実際の運用で得た気づきを整理しています。

AIにコードを書かせて終わりではなく、どこで迷ったか・どこを人がレビューすべきかまで含めて、 やさしく噛みくだいてまとめています。

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

得意:PHP・JavaScript・MySQL・CSS

制作・運用中:フォーム生成基盤クイズ学習プラットフォームhtmx逆引きレシピ

AI時代のエンジニアタイプ診断:CSPF/とろとろみかん

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

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

もちもちみかん.comとは


もちもちみかん.com は、AI時代に必要なプログラミングの基礎・設計・改善の考え方を、やさしく学べるサイトです。


『プリンシプル オブ プログラミング』の原則まとめ、用語集、クイズ、htmx逆引きレシピを通じて、「分かったつもり」で終わらず、実際に使える判断軸まで身につくようにしています。


フォームジェネレーターなどの便利ツールも用意しつつ、AIにコードを書かせる時代にこそ大切な設計・責務・レビューの視点を、実務と検証の目線で届けます。


むずかしい内容でも親しみやすく学べるように、「もちもちみかん」らしいやわらかさも大切にしています!