コンテンツにスキップ

データ領域

データ領域は、自身の更新ライフサイクルを所有する UI の一部です。 ページ描画時に一度読み込み、その後はポーリング、イベントの待ち受け、 またはその組み合わせで自身を最新に保ちます。コンポーネントはただの htmx 駆動コンテナで、ビヘイビアヘルパーは不要です。

別名: 自動更新、ポーリング。

Simulate a change elsewhere を押してください — エンドポイントは 204HX-Trigger: {"items:changed":{}, "hc:toast":…} で応答し、 イベントを聞いた領域が自分自身を再取得します(Rendered at の時刻が 変わるのが見えます)。エンドポイントはサーバレスポンス契約を実装した 名前空間付きデモで、api/recipes/data-region/ 配下にあります。

2 つ目の領域(Polling region)は スケジュールによるポーリングのデモ です: 10 秒ごとに自身を再取得します — 再描画されるフラグメントは every 10s だけを持ち、htmx がスワップのたびにインターバルを 再アームします。Rendered at の行が刻むのを見てください。

Items

Polling region

スケジュールによるポーリング

Section titled “スケジュールによるポーリング”
<section id="orders-summary"
class="hc-data-region"
data-hx-get="/orders/summary"
data-hx-trigger="load, every 10s"
data-hx-swap="outerHTML"
data-hx-indicator="closest .hc-data-region"
aria-busy="false">
<header class="hc-data-region__header">
<h2>Orders</h2>
<span class="hc-spinner htmx-indicator" aria-hidden="true"></span>
</header>
<p class="hc-field__message">Loading…</p>
</section>

構成要素:

  • data-hx-trigger="load, every 10s" — ページロード時に一度描画し、 以後 10 秒ごとに再描画。サーバが再描画するフラグメントは every 10s だけを持ちます: htmx はスワップのたびに インターバルを再アームし(その再スタートこそがポーリングです)、 load をエコーバックすると永遠に再取得してしまいます。
  • data-hx-swap="outerHTML" — 領域はサーバが返す完全な <section>(同じ id、クラス、属性)で自分自身を置き換えます。 そのためどのスワップも冪等で、領域のトリガーの主導権はサーバに 残ります。
  • data-hx-indicator="closest .hc-data-region" — 遅いポーリングでは コンテンツをちらつかせる代わりに、ヘッダーの .hc-spinner.htmx-indicator が点灯します。

ポーリングされるエンドポイントは常に安価(高速な COUNT クエリや 非正規化された読み取りモデル)かつ冪等に設計してください。

内側コンテンツのフラグメント(innerHTML)

Section titled “内側コンテンツのフラグメント(innerHTML)”

内側のコンテンツだけを返す(ラッパー要素のない)既存の フラグメントエンドポイントを後付け(レトロフィット)する場合は、 トリガーを素のコンテナに置いたまま、中身をスワップさせます:

<section id="orders-summary"
data-hx-get="/orders/summary"
data-hx-trigger="load, every 10s"
data-hx-swap="innerHTML">
<p class="hc-field__message">Loading…</p>
</section>
  • コンテナ自体(トリガーごと)はそのまま残り、中身だけが置き換わる ため、再アームするものはなく、トリガーは書いたとおりに時を刻み 続けます。
  • トレードオフ: フラグメントが属性を運ばないため、サーバは領域を 調整できなくなります — ポーリング間隔の変更、ポーリングの停止、 新しいイベントの待ち受けなど。サーバを制御できるなら上の 自己置換型 hc-data-region 形式を選び、この形式は レトロフィットのために取っておいてください。

エンドポイントがフルページしか返せない(フラグメント用ルートの ないサーバレンダリングビュー)場合は、領域にそのページから自身の 置き換え分を切り出させ、自分をスワップさせます:

<div id="page-content"
data-hx-get="/ops/console/outbox"
data-hx-trigger="every 15s"
data-hx-select="#page-content"
data-hx-target="this"
data-hx-swap="outerHTML">
</div>
  • data-hx-select="#page-content" がレスポンスから一致する要素を抽出 し、data-hx-swap="outerHTML" がトリガーごと領域を置き換えます。
  • これは 1 ティックごとにフルページ描画のコストがかかります — サーバを 制御できるならフラグメントエンドポイント(自己置換型 hc-data-region 形式、または上の innerHTML 形式)を選び、 hx-select は後付け(レトロフィット)のために取っておいて ください。
  • outerHTML では、領域の every 15s タイマーはスワップのたびに 再スタートします。トリガーに load がなければ初回描画はサーバ レンダリングされた HTML なので、フラッシュはありません。

ポーリングを htmx カスタムイベントと組み合わせれば、サーバの アクションが領域に即時更新を求められます:

<section id="orders-summary"
class="hc-data-region"
data-hx-get="/orders/summary"
data-hx-trigger="load, every 30s, orders:refresh from:body"
data-hx-swap="outerHTML"
data-hx-indicator="closest .hc-data-region"
aria-busy="false">
</section>

サーバは任意の書き込みエンドポイントで HX-Trigger からイベントを 発火します:

HTTP/1.1 200 OK
HX-Trigger: {"hc:toast":{"message":"Created"},"orders:refresh":true}

from:bodydocument.body 上のイベントを待ち受けます — HX-Trigger のイベントを htmx が発火する場所です。

reveal 駆動(初回読み込みの遅延)

Section titled “reveal 駆動(初回読み込みの遅延)”

ファーストビューより下のデータ領域では、領域がスクロールで見えるまで 初回取得を先送りします:

<section id="invoices-summary"
class="hc-data-region"
data-hx-get="/invoices/summary"
data-hx-trigger="intersect once, every 30s"
data-hx-swap="outerHTML"
data-hx-indicator="closest .hc-data-region"
aria-busy="false">
<p class="hc-field__message">Loading when visible…</p>
</section>
  • intersect once は要素がビューポートに入ったとき、一度だけ発火。
  • 後続の every 30s は初回取得の後にだけ時を刻み始めます。

サーバは完全な <section> — 同じ id、同じクラス、同じ属性 — を返し、スワップを冪等にします。意図的な違いは 1 つだけ: 再描画されるフラグメントはトリガーから load を落とします (htmx はスワップされたばかりの要素すべてで load を発火するため、 エコーバックすると永遠に再取得してしまいます):

<!-- GET /orders/summary -->
<section id="orders-summary"
class="hc-data-region"
data-hx-get="/orders/summary"
data-hx-trigger="every 10s"
data-hx-swap="outerHTML"
data-hx-indicator="closest .hc-data-region"
aria-busy="false">
<header class="hc-data-region__header">
<h2>Orders</h2>
<span class="hc-spinner htmx-indicator" aria-hidden="true"></span>
</header>
<dl class="hc-stack">
<div>
<dt>Open orders</dt>
<dd>14</dd>
</div>
<div>
<dt>Awaiting payment</dt>
<dd>3</dd>
</div>
</dl>
</section>

フラグメントが領域の属性を運ぶため、主導権はサーバに残ります: ポーリング間隔を変える、データが確定したらポーリングを止める、 行がなくなったら明示的な空状態を描画する、といったことができます。 innerHTML のレトロフィット形式 では、代わりに内側のコンテンツ(上の <dl>)だけを返します。

htmx(HX-Request: true)とフルページロードの両方に同じコンテンツを 返してください。キャッシュは標準の Cache-Control ヘッダーで調整 できます。htmx はそれを尊重します。

リクエストレスポンス
GET /orders/summary(ポーリング・イベント・reveal)200 + 完全な <section>(innerHTML レトロフィットでは内側コンテンツだけ)
GET /orders/summary(HX-Request なし — フルページロード)200 + 同じコンテンツをフルページで包んだもの
領域を更新させたい任意の書き込みエンドポイント2xx + HX-Trigger: {"orders:refresh":true}
GET /orders/summary(失敗)非 2xx — スワップされず、領域は直前の描画を保ちます

htmx ≥ 2 は非 2xx レスポンスをスワップしないため、ポーリングが失敗 しても直前の描画が見えたままです — 領域は「少し古い」に劣化するだけで、 壊れることはありません。ユーザーに知らせるには、エラーレスポンスに HX-Trigger: {"hc:toast":{…}} ヘッダーを付けてください (トーストレシピを参照)。

ポーリングのエンドポイントが遅く、コンテンツのちらつきが問題になる なら、htmx-indicator でスピナーを包みます。上の hc-data-region 形式は data-hx-indicator="closest .hc-data-region" ですでにこれを 配線済みです。innerHTML 形式では、インジケーターのセレクタが スピナーに届くよう、スピナーを含む要素にリクエスト属性を置いて ください(closest は祖先だけを遡り、find は子孫を探索します):

<section id="status"
data-hx-get="/status"
data-hx-trigger="load, every 5s"
data-hx-target="#status-body"
data-hx-swap="innerHTML"
data-hx-indicator="find header .hc-spinner">
<header>
<h2>Sync status</h2>
<span class="hc-spinner htmx-indicator" aria-hidden="true"></span>
</header>
<div id="status-body"></div>
</section>
  • 変化を知らせることがユーザーの利益になる場合(カウンターなど)は 領域に aria-live="polite" を、更新が騒がしく装飾的なら aria-live="off" を付けてください。
  • 更新のたびにフォーカスを移動しないでください。フォーカスを 乗っ取るポーリングは、キーボードユーザーにもスクリーンリーダーにも 敵対的です。
  • データセットが空になったら明示的な空状態を表示し、支援技術が静かに 空になったセクションに出くわさないようにしてください。

プログレッシブエンハンスメント

Section titled “プログレッシブエンハンスメント”
  • JavaScript なしでは、領域は初期のサーバサイドコンテンツを描画し、 更新されません。空白のままにせず、妥当なデフォルトをセクション内に 描画してください。
  • htmx なしでは、セクションは素の HTML として振る舞います。
  • バックグラウンドタブはタイマーを絞ります。 ブラウザは非アクティブ なタブでポーリングを遅くします。リアルタイム精度を every 5s に 頼らないでください。
  • 同期ポイント。 秒未満の鮮度が重要なら、ポーリングを SSE や WebSocket と組み合わせます。データ領域のレシピは同じままで、 トリガーだけが変わります。
  • コスト。 スケールでの毎秒ポーリングは高くつきます。バックプレーン を持ったら、イベント駆動更新(from:body)を選んでください。