Back

HTMXで無限スクロールを構築する

HTMXで無限スクロールを構築する

ユーザーがスクロールするとコンテンツを読み込む無限スクロールは、ページネーションのクリックを不要にし、よりスムーズなブラウジング体験を実現します。しかし、従来の方法で無限スクロールを実装するには、Intersection Observerの管理、状態の追跡、そして相当量のJavaScriptコードを記述する必要があります。HTMXは、HTML属性を使用したサーバー駆動の無限スクロールという、よりシンプルなアプローチを提供します。

このガイドでは、HTMXの標準的な無限スクロールパターンを説明し、revealedintersectの使い分けを解説し、JavaScriptなしでも動作するプログレッシブエンハンスメントの実装方法を紹介します。

重要なポイント

  • HTMXの無限スクロールは、各サーバーレスポンスに次のローダーを含める自己永続型ローダー要素パターンを使用します
  • フルページスクロールにはrevealedを、オーバーフローのあるスクロール可能なコンテナにはintersect onceを使用します
  • JavaScriptなしでも動作するプログレッシブエンハンスメントのために、標準的なページネーションリンクを基盤として構築します
  • サーバーは各レスポンスのローダー要素を通じて、すべてのページネーション状態を制御します

サーバー駆動の無限スクロールの仕組み

コアパターンは、「ローダー」要素(通常はリストの最後のアイテム)にHTMX属性を配置します。この要素が表示されると、HTMXはサーバーに次のページをリクエストします。サーバーは、新しいアイテムと次のページを指す新しいローダー要素の両方を含むHTMLを返します。

基本的な構造は次のとおりです:

<div id="items">
  <div class="item">First item</div>
  <div class="item">Second item</div>

  <!-- Last rendered item doubles as the loader -->
  <div class="item"
       hx-get="/items?page=2"
       hx-trigger="revealed"
       hx-swap="afterend">
    Last item of page 1
  </div>
</div>

ページ2のサーバーレスポンスには、アイテムと新しいローダーが含まれます:

<div class="item">First item of page 2</div>
<div class="item">Second item of page 2</div>

<div class="item"
     hx-get="/items?page=3"
     hx-trigger="revealed"
     hx-swap="afterend">
  Last item of page 2
</div>

この自己永続型パターンは、サーバーがローダーなしのコンテンツを返すまで続き、利用可能なデータの終わりを示します。

HTMX IntersectとRevealed:適切なトリガーの選択

HTMXは2つの可視性ベースのトリガーを提供しており、間違ったものを選択すると一般的なバグが発生します。

revealedを使用するのは、ドキュメント自体がスクロールするフルページスクロールの場合です。このトリガーは、要素がブラウザのビューポートに入ったときに発火します。

intersect onceを使用するのは、コンテンツがoverflow-y: scrollのようなCSSを持つスクロール可能なコンテナ内にある場合です。revealedトリガーはドキュメントのビューポートを監視し、個別のスクロール可能な要素は監視しないため、オーバーフローコンテナでは正しく動作しません。intersectトリガーは、すべての主要ブラウザでサポートされている最新のIntersection Observer APIに依存しています。

<!-- Full-page scroll: use revealed -->
<div hx-trigger="revealed" hx-get="/more">...</div>

<!-- Scrollable container: use intersect once -->
<div class="scroll-container" style="overflow-y: scroll; height: 400px;">
  <div hx-trigger="intersect once" hx-get="/more">...</div>
</div>

once修飾子は、要素が交差しきい値を繰り返し出入りする場合の重複リクエストを防ぎます。

スワップ戦略:afterendとbeforeend

スワップ戦略は、新しいコンテンツがどこに表示されるか、および重複読み込みを防ぐ方法を決定します。

**afterend**は、ローダー要素の直後にコンテンツを挿入します。ローダーが最後にレンダリングされたアイテムである場合に使用します—レスポンスはその下に表示されます。

**beforeend**は、ターゲットコンテナ内にコンテンツを追加します。これをhx-targetと組み合わせて、アイテムの配置場所を指定します:

<div hx-get="/items?page=2"
     hx-trigger="revealed"
     hx-target="#items"
     hx-swap="beforeend">
  Loading indicator...
</div>

beforeendを使用する場合、ローダーは通常アイテムコンテナの外側に配置され、最終ページが読み込まれた後に置き換えられるか削除されます。

標準的なページネーションによるプログレッシブエンハンスメント

HTMXによる無限スクロールは、既存のページネーションを完全に置き換えるのではなく、その上に構築する必要があります。まず、動作するページネーションリンクから始めます:

<div id="items">
  {% for item in items %}
    <div class="item">{{ item.name }}</div>
  {% endfor %}
</div>

<a href="/items?page={{ next_page }}">Load more</a>

次に、HTMX属性でページネーションリンクをアップグレードします:

<a href="/items?page={{ next_page }}"
   hx-get="/items?page={{ next_page }}"
   hx-trigger="revealed"
   hx-target="#items"
   hx-swap="beforeend"
   hx-select="#items > *">
  Load more
</a>

JavaScriptを使用しないユーザーには標準的なページネーションが表示されます。HTMXを使用するユーザーには無限スクロールが提供されます。サーバーエンドポイントは両方で同一のままです。

Out-of-Band SwapsによるUI要素の更新

メインコンテンツエリア外の要素(アイテムカウンターや読み込み状態など)を更新する必要がある場合があります。HTMX out-of-band swapsがこれを処理します:

<!-- In server response -->
<div class="item">New item</div>
<span id="item-count" hx-swap-oob="outerHTML">Showing 20 of 100</span>

hx-swap-oob="outerHTML"属性は、メインスワップターゲットに関係なく、ページ上のどこでも一致するIDを持つ要素を見つけて置き換えるようHTMXに指示します。

サーバーサイドのページネーション戦略

サーバーはページネーション状態を完全に制御します。一般的なアプローチは2つあります:

オフセットページネーションは、ページ番号またはオフセット値を使用します:/items?page=3または/items?offset=20。実装は簡単ですが、ブラウジング中にアイテムが追加されると重複が表示される可能性があります。

カーソルページネーションは、最後に表示されたアイテムへのポインタを使用します:/items?after=item_xyz。動的コンテンツに対してより信頼性が高いですが、安定した識別子が必要です。

どちらのアプローチもHTMXで機能します—クライアントは、サーバーが次のローダー要素で提供するパラメータを単純に渡すだけです。

まとめ

HTMXの無限スクロールは、ページネーションロジックが既に存在するサーバーに複雑さを移動します。ドキュメントスクロールにはrevealedを、オーバーフローコンテナにはintersect onceを選択してください。機能が適切に劣化するように、標準的なページネーションの上に構築してください。クライアント側でページを追跡するのではなく、各レスポンスのローダー要素を通じてサーバーに状態を駆動させます。

よくある質問

これは通常、once修飾子なしでintersectを使用した場合に発生します。コンテンツが読み込まれてレイアウトがシフトすると、要素が交差しきい値を繰り返し越える可能性があります。hx-trigger='intersect once'のようにトリガーにonceを追加して、ローダー要素ごとに1回のリクエストのみが発火するようにしてください。

読み込み要素を指すhx-indicator属性を追加します。例えば、ローダー要素にhx-indicatorを値'#loading'で設定し、スピナーを含む別のspanをid='loading'で用意します。HTMXはリクエスト中に自動的にこの要素を表示し、完了時に非表示にします。

HTMXは重複リクエストの防止を支援し、各ローダー要素は通常一度だけトリガーするように設定されています。サーバーレスポンスが次のページを決定します。より厳密な調整が必要な場合は、hx-syncを使用してリクエストの動作を制御できます。

はい。フィルタが変更されたら、コンテンツコンテナをリセットし、フィルタパラメータを含むようにローダーURLを更新します。サーバーはフィルタリングロジックを処理し、適切な最初のページを返します。後続の各ローダーには、ページ間で一貫性を維持するために同じフィルタパラメータが含まれます。

Gain Debugging Superpowers

Unleash the power of session replay to reproduce bugs, track slowdowns and uncover frustrations in your app. Get complete visibility into your frontend with OpenReplay — the most advanced open-source session replay tool for developers. Check our GitHub repo and join the thousands of developers in our community.

OpenReplay