はじめに
無限ローディング(無限スクロール)は、Webサイトやアプリケーションで使用される機能です。ユーザーがページの最下部までスクロールすると、自動的に新しいコンテンツが読み込まれる仕組みです。この機能により、ユーザーは「次へ」ボタンをクリックすることなく、継続的にコンテンツを閲覧できます。
これは、HotwireのTurboの機能を使えば簡単に実装できます。
例えば、以下のサイトで提案されている方法です。これは、Turbo Framesの遅延読み込みのみを使った方法です。
この方法とは異なる実装方法を、同僚の@kei-p さんに教えてもらいました。これは、Turbo Framesの遅延読み込みとTurbo Streamsの両方を使った方法です。この実装方法について書かれた記事が見当たらなかったので、ここでまとめたいと思います。
環境
- Ruby 3.3.6
- Ruby on Rails 8.0.0
- turbo-rails 2.0.11
Turbo Framesの遅延読み込みだけを使う方法
記事一覧を無限ローディングする例を使って、Turbo Framesの遅延読み込みだけで実装する方法について説明します。
この画面は、以下のcontroller, viewで実現しています。
class ArticlesController < ApplicationController
def index
page_per = 10
@current_page = params[:page].to_i
@articles = Article.where('id >= ?', @current_page * page_per)
.order(:id)
.limit(page_per)
@next_page = @current_page + 1
end
end
<h1>記事一覧</h1>
<ul>
<%= turbo_frame_tag "articles-page-#{@current_page}" do %>
<% @articles.each do |article| %>
<li>
<%= link_to article.title, article_path(article) %>
</li>
<% end %>
<!-- まだ表示されていない記事を遅延読み込み -->
<%= turbo_frame_tag "articles-page-#{@next_page}", loading: :lazy, src: articles_path(page: @next_page) %>
<% end %>
</ul>
11行目のturbo_frame_tag
で、まだ表示されていない記事を遅延読み込みしています。この行により以下のようなHTMLが生成されます。このHTMLがブラウザ上で見える位置にある時、src
属性で指定してURLを読み込みます。loading
属性の値を"lazy"
にすると遅延読み込みされます。
<turbo_frame id="articles-page-1" loading="lazy" src="/articles?page=1">
</turbo_frame>
この<turbo_frame>
タグが、この遅延読み込みのレスポンスにあるHTMLと置き換わります。レスポンスは以下のようになるので、id
がarticles-page-1
のHTMLに置き換わります。
<head>
</head>
<body>
<h1>記事一覧</h1>
<ul>
<!-- ここから -->
<turbo-frame id="articles-page-1">
<li>
<a href="/articles/10">記事タイトル 8</a>
</li>
<li>
<a href="/articles/11">記事タイトル 9</a>
</li>
<li>
<a href="/articles/12">記事タイトル 10</a>
</li>
<!-- 省略 -->
<turbo-frame loading="lazy" id="articles-page-2" src="/articles?page=2"></turbo-frame>
</turbo-frame>
<!-- ここまで -->
</ul>
</body>
</html>
つまり、HTMLは以下のようになります。
<!-- 省略 -->
<ul>
<li>
<a href="/articles/10">記事タイトル 0</a>
</li>
<li>
<a href="/articles/11">記事タイトル 1</a>
</li>
<li>
<a href="/articles/12">記事タイトル 2</a>
</li>
<!-- 省略 -->
<turbo-frame id="articles-page-1">
<li>
<a href="/articles/10">記事タイトル 8</a>
</li>
<li>
<a href="/articles/11">記事タイトル 9</a>
</li>
<li>
<a href="/articles/12">記事タイトル 10</a>
</li>
<!-- 省略 -->
<turbo-frame loading="lazy" id="articles-page-2" src="/articles?page=2"></turbo-frame>
</turbo-frame>
</ul>
このように<ul>
内のネストが一段増えます。意図的にネストを増やしたわけではありません。無限ローディングを実現したいがために、仕方なくネストが一段増えてしまっています。<ul>
の子要素は<li>
であってほしいです。
Turbo Framesの遅延読み込みとTurbo Streamsを使う方法
Turbo Framesの遅延読み込みと、Turbo Streamsを使う方法であれば、<ul>
内のネストを余計に増やすことなく、無限ローディングを実現できます。
この実装では、記事へのリンクを表示する部分と、遅延読み込みを行うところで分けています。
<h1>記事一覧</h1>
<!-- 記事へのリンクを表示 -->
<ul id="articles" >
<%= render 'articles', articles: @articles %>
</ul>
<!-- 記事へのリンクを表示 -->
<!-- 遅延読み込み -->
<%= render 'loading', page: @next_page %>
<!-- 遅延読み込み -->
<% articles.each do |article| %>
<li>
<%= link_to article.title, article_path(article) %>
</li>
<% end %>
遅延読み込みを行う部分は前項とほとんど同じですが、src
属性で渡しているURLのformatをturbo_stream
に変更しているところだけ異なります。
<%= turbo_frame_tag :articles_loading, loading: :lazy, src: articles_path(page:, format: :turbo_stream) do %>
読み込み中...
<% end %>
turbo_stream
フォーマットでArticlesController#index
アクションが実行されるので、以下のファイルがレンダリングされます。2行目でid
がarticles
の要素、つまり<ul>
に記事へのリンクが追加されます。そして、3行目で遅延読み込みを行う要素を置き換えています。
<% if @articles.present? %>
<%= turbo_stream.append :articles, partial: 'articles', locals: { articles: @articles} %>
<%= turbo_stream.replace :articles_loading, partial: 'loading', locals: { page: @next_page} %>
<% else %>
<%= turbo_stream.replace :articles_loading %>
<% end %>
この方法で無限ローディングした場合、記事一覧は以下のようなHTMLになります。このように<ul>
内のネストを余計に増やすことなく、無限ローディングを実現できます。
<ul id="articles">
<li>
<a href="/articles/2">記事タイトル 0</a>
</li>
<li>
<a href="/articles/3">記事タイトル 1</a>
</li>
<li>
<a href="/articles/4">記事タイトル 2</a>
</li>
<!-- 省略 -->
<li>
<a href="/articles/27">記事タイトル 100</a>
</li>
</ul>
<turbo-frame loading="lazy" id="articles_loading" src="/articles.turbo_stream?page=2">
読み込み中...
</turbo-frame>
おわりに
Hotwireで無限ローディングを実装する2つの方法をまとめました。
- Turbo Framesの遅延読み込みだけを使う方法
- Turbo Framesの遅延読み込みとTurbo Streamsを使う方法
1つ目の方法は、Turbo Framesの遅延読み込みについて知っていれば実装できるので、学習コストが少ないです。一方、2つ目の方法は、Turbo Streamsについての知識も必要ですが、HTMLに余計なネストを増やさないので、HTMLの複雑度が小さくなります。
他の実装方法があれば知りたいので、よければ、コメントで教えてほしいです🙏