SSE ライブ更新
sse-updates はサーバプッシュ UI のための公認パターンです:
ユーザーが操作したからではなく、サーバがそう言ったから再描画される
フィード、ステータスパネル、データグリッド。
htmx SSE 拡張が
EventSource を所有し、マークアップがストリームとイベント名を宣言し、
サーバはデータが完成済みフラグメントである名前付きイベントを送り
ます — 他のすべてのレシピと同じ「マークアップこそワイヤ契約」の立場
です。マークアップのバージョニングポリシーの
下で安定しています。
フラグメントのプッシュに Hypermedia Components のビヘイビアは不要 です — サーバプッシュの通知とドメインイベントには SSE トーストレシピを 参照してください。
別名: リアルタイム更新、プッシュ更新。
ストリームは約 25 秒のスクリプト済みシーケンスを一度だけ再生し、
stream:done で意図的に自ら終了します: タイムスタンプつきの
アクティビティ項目がフィードの先頭に追加され、ステータスパネルが
再描画され、1 つのプッシュが同じイベントの中でアラートバッジも
out-of-band で更新し、さらに 2 回の products:rows プッシュが
データグリッドの tbody に行 1 ページ分をスワップします
(下記の合成)。シーケンスはページ
読み込み時に始まります — 終わってしまっていたら
Replay the stream が新しいスコープ(新しい EventSource)を
スワップして再実行します。
Waiting for updates…
Alerts:0
| ID | Status | Updated |
|---|---|---|
| — | Waiting for pushed rows… | — |
マークアップ
Section titled “マークアップ”<div data-hx-ext="sse" data-sse-connect="/events" data-sse-close="stream:done"> <ul id="activity" data-sse-swap="activity:item" data-hx-swap="afterbegin"> <li class="hc-item">…server-rendered current items…</li> </ul>
<section id="status" data-sse-swap="status:panel"> …server-rendered current status… </section></div>ストリームごとに 1 つの data-sse-connect スコープ。内側の各要素は
受け取るイベントを data-sse-swap で名指しし、イベントのデータは
その要素の data-hx-swap を尊重してスワップされます(デフォルトは
innerHTML、フィードには afterbegin)。拡張は data- プレフィックス
形の属性をサポートし、キットの data-hx-* 規約と揃います。
retry: 5000
event: activity:itemdata: <li class="hc-item">Deploy #42 started</li>
event: status:paneldata: <p>All systems normal</p><span id="alert-badge" data-hx-swap-oob="true">3</span>イベントはドメインで命名します(activity:item、status:panel)—
名前はワイヤ契約です。メッセージのデータは、同じプッシュの中で id に
よって他のターゲットを更新する data-hx-swap-oob フラグメントも運べます
(2 つ目の例はパネルとバッジの両方を更新します)。
datagrid-pager や
一括操作のレスポンスとまったく同じです。
サーバレスポンス契約
Section titled “サーバレスポンス契約”GET /events は Content-Type: text/event-stream で応答します。各更新は
名前付きイベントで、その data: は 1 行に収めた完成済みの
サーバレンダリングフラグメントです。HX-Request での分岐は
ありません — EventSource のリクエストは htmx のヘッダーを運ばない
ため、ストリームがこのエンドポイントの唯一の形です。
| リクエスト | レスポンス |
|---|---|
GET /events | 200 + Content-Type: text/event-stream — 名前付きイベント(event: activity:item)、data: 行ごとに完成済みフラグメント 1 つ |
| 切断後の再接続 | 同じストリーム — EventSource がネイティブにリトライします(サーバの retry: ヒントを尊重)。現在の状態を再送するか、欠落を許容してください |
| 意図的な終了 | data-sse-close が名指しするイベント(ここでは stream:done)を送ります — 閉じられたストリームは閉じたままで、終了イベント自身のペイロードは決してスワップされません |
| クロスオリジンのストリーム | 拡張は withCredentials: true で接続するため、サーバには Access-Control-Allow-Credentials が必要です |
データグリッドとの合成
Section titled “データグリッドとの合成”.hc-datagrid__body の tbody に data-sse-swap +
data-hx-swap="innerHTML" を置けば、行 1 ページ分をプッシュできます。
ページャーの keep-the-tbody ルールがそのまま適用されます:
グリッドのオブザーバーはスワップを生き延び、roles と sticky
オフセットを再適用し、選択を導出し直します — そのため
選択アクションバーは、
プッシュされた行が届くと追加の配線なしで自分をクリアします。
ライフサイクル
Section titled “ライフサイクル”再接続は htmx ネイティブです: EventSource は自動でリトライし
(サーバの retry: ヒントを尊重)、拡張は連続失敗にバックオフを追加
します — したがってサーバは再接続を予期して現在の状態を再送するか、
欠落を許容しなければなりません。ストリームは
data-sse-close="<event>" で意図的に終了します(落ちたストリームは
再接続し、閉じられたストリームは閉じたままです)。デバッグ用に
htmx:sseOpen / htmx:sseError / htmx:sseClose がスコープ上で発火
します。拡張は withCredentials: true で接続します — クッキーが同乗
します。
非 JS デグラデーション
Section titled “非 JS デグラデーション”初期 HTML に完全な現在の状態を描画してください。ストリームはそれを 新鮮にするだけです。JS なしでは、ページは単に最後のフル描画の時点の 鮮度になります — ローディングシェルなし、何も壊れません。
ここでの主張は実 EventSource のブラウザテスト
(test-browser/sse.spec.mjs)で固定されています。
アクセシビリティ
Section titled “アクセシビリティ”- プッシュされたフラグメントはフォーカスを動かさずにコンテンツを 置き換えます — ユーザーが現在フォーカスしている要素をライブ リージョンで包まないでください。
- 読み上げはオプトインです: フラグメントが着地する領域に
aria-live="polite"を置いてください。ステータス行はたいてい 読み上げるべきで、フィードは項目を逐一読み上げるべきでは ありません。 - サーバプッシュの通知には
SSE トーストレシピを
使ってください — トーストは正しい
role="status"/role="alert"を 最初から備えています。 - プッシュされたデータグリッドの行はグリッドの通常の再初期化を通る ため、roles とキーボードモデルはスワップのたびに生き残ります。
- SSE トーストレシピ — ディスパッチブリッジによるサーバプッシュの通知とドメインイベント。
- htmx インテグレーション — Server-Sent Events — 拡張の読み込み、ライフサイクル、クレデンシャル。
- データグリッドのページネーションレシピ — これが合成する tbody スワップのルール。
- データ領域レシピ — プル型更新という補完パターン。