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としてグループ企業向けの業務アプリを要件定義〜運用まで一気通貫で担当しています。

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

得意:PHP・JavaScript・MySQL・CSS

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

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

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

もちもちみかん.comとは


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

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