2
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

HotwireAdvent Calendar 2024

Day 2

Hotwireで無限ローディング

Last updated at Posted at 2024-12-01

はじめに

無限ローディング(無限スクロール)は、Webサイトやアプリケーションで使用される機能です。ユーザーがページの最下部までスクロールすると、自動的に新しいコンテンツが読み込まれる仕組みです。この機能により、ユーザーは「次へ」ボタンをクリックすることなく、継続的にコンテンツを閲覧できます。

これは、HotwireTurboの機能を使えば簡単に実装できます。

例えば、以下のサイトで提案されている方法です。これは、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の遅延読み込みだけで実装する方法について説明します。

Dec-01-2024 21-00-07.gif

この画面は、以下のcontroller, viewで実現しています。

app/controllers/articles_controller.rb
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
app/views/articles/index.html.erb
<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と置き換わります。レスポンスは以下のようになるので、idarticles-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>内のネストを余計に増やすことなく、無限ローディングを実現できます。

この実装では、記事へのリンクを表示する部分と、遅延読み込みを行うところで分けています。

app/views/articles/index.html.erb
<h1>記事一覧</h1>

<!-- 記事へのリンクを表示 -->
<ul id="articles" >
  <%= render 'articles', articles: @articles %>
</ul>
<!-- 記事へのリンクを表示 -->

<!-- 遅延読み込み -->
<%= render 'loading', page: @next_page %>
<!-- 遅延読み込み -->
app/views/articles/_articles.html.erb
<% articles.each do |article| %>
  <li>
    <%= link_to article.title, article_path(article) %>
  </li>
<% end %>

遅延読み込みを行う部分は前項とほとんど同じですが、src属性で渡しているURLのformatをturbo_streamに変更しているところだけ異なります。

app/views/articles/_loading.html.erb
<%= turbo_frame_tag :articles_loading, loading: :lazy, src: articles_path(page:, format: :turbo_stream) do %>
  読み込み中...
<% end %>

turbo_streamフォーマットでArticlesController#indexアクションが実行されるので、以下のファイルがレンダリングされます。2行目でidarticlesの要素、つまり<ul>に記事へのリンクが追加されます。そして、3行目で遅延読み込みを行う要素を置き換えています。

app/views/articles/index.turbo_stream.erb
<% 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つの方法をまとめました。

  1. Turbo Framesの遅延読み込みだけを使う方法
  2. Turbo Framesの遅延読み込みとTurbo Streamsを使う方法

1つ目の方法は、Turbo Framesの遅延読み込みについて知っていれば実装できるので、学習コストが少ないです。一方、2つ目の方法は、Turbo Streamsについての知識も必要ですが、HTMLに余計なネストを増やさないので、HTMLの複雑度が小さくなります。

他の実装方法があれば知りたいので、よければ、コメントで教えてほしいです🙏

2
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
2
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?