Turboを使った無限スクロールが簡単すぎて感動した

# はじめに
本記事は錆びかけたRailsの知識を頑張ってアップデートするアドベントカレンダー12日目です。

Turboを使うと、無限スクロールの実装が驚くほど簡単にできます。

### 無限スクロールとは
ある程度スクロールすると続きのコンテンツがロードされるようなUIです。

一番馴染み深いのはTwitter(X)のアプリの挙動です。タイムラインをスクロールしていくと、自動で続きが読み込まれますね。

Turboを利用した無限スクロールの実装を見た時、あまりの簡単さに感動したので、やり方をチュートリアル形式でごく簡単に紹介します。

### 対象読者
Ruby on Railsを学習中の人
Railsですでに簡単なアプリケーションを作成したことがある人
※まだRailsについてよくわかっていないよ、という方は対象ではありません。

# 事前準備
以下の環境で実装を行います。必要に応じて環境構築してください。

:::note info 
- Ruby 3.2.0
- Rails 7.0.5
- MySQL 8系
- OS: MacOS Monterey バージョン 12.6.2
:::

以下の記事はDockerで上記の環境を作る記事となっています。
[Docker for MacをインストールしてRails7 + MySQL8の環境を作る](https://qiita.com/eichann/items/1b7683584b6f88593513)

ここでは、Rails7系で`rails new`して、アプリの雛形ができていると想定します。Rubyの該当バージョンやRails7系がインストールできていれば

### ローカルサーバの立ち上げ方に注意
Railsはこれまで`rails s`コマンドを使ってローカルサーバを立ち上げていました。Rails7からは、`bin/dev`というコマンドで立ち上げるようになっています。

`bin/dev`というコマンドについては以下の記事を参照してください。
[bin/devって何だ](https://qiita.com/eichann/items/38968db7c7aa13c1f978)

bin/devコマンドを使ってローカルサーバを立ち上げ、Google Chromeなどのブラウザから`localhost:3000`にアクセスして以下のように表示されていれば準備OKです。

![fejwaau.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/65439/0e849c19-0167-d8b4-4f45-d71fde91b3df.png)


# Turboによる無限スクロール体験ハンズオン

## 作るもの

![202312141.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/65439/c10b0af0-b94d-3cbf-7183-0692f6559513.gif)


## 手順
tweetモデルとテーブルを作成する
tweetsテーブルにダミーデータを100件投入する
一覧ページでtweet一覧を100件表示する
ページネーションを実装する
無限スクロールを実装する

### tweetモデルとテーブルを作成する
まずは、無限スクロールを試すリソースを用意します。今回はtweetというモデルとし、表示する中身はcontentという属性値にします。以下のコマンドで、モデルとマイグレーションファイルを作成します。

```bash:tweetモデルの作成
docker compose run i-scroll-web rails g model tweet content:string
```

続いてマイグレーションファイルを読み込みテーブルを作成します。

```bash:tweetモデルの作成
docker-compose run i-scroll-web rails db:migrate
```

### tweetsテーブルにダミーデータを100件投入する

tweetsテーブルを作成できたので、ダミーデータを投入します。

今回は`gem faker`を使います。

```shell:Gemfile
// Gemfileの一番下に追記
gem "faker"
```

bundle installします。

```bash:ターミナル
docker-compose run i-scroll-web bundle
```

seeds.rbの中身を以下のように書き換えます。

```ruby
require 'faker'

100.times do |i|
  Tweet.create(content: Faker::Lorem.sentence(word_count: 5), nickname: Faker::Internet.username)
end
```

作成したseeds.rbを実行します。これで、tweetsテーブルにレコードが100件保存されます。

```bash:ターミナル
docker-compose run i-scroll-web rails db:seed
```

### 一覧ページでtweet一覧を100件表示する
続いて一覧ページを作成します。コントローラやviewは`scaffold`で作成します。

```bash:ターミナル
rails g scaffold_controller Tweet
```

これで、必要なルーティング/コントローラ/ビューの準備が整います。以下のようなファイルが作成されていればOKです。
手動で作成しても構いません。

```ruby:routes.rb
Rails.application.routes.draw do
  resources :tweets
end
```

```ruby:tweets_controller.rb
class TweetsController < ApplicationController
  before_action :set_tweet, only: %i[ show edit update destroy ]

  # GET /tweets
  def index
    @tweets = Tweet.all
  end

 # 以下略
end

```

```erb:views/tweets/index.html.erb
<p style="color: green"><%= notice %></p>

<h1>Tweets</h1>

<div id="tweets">
  <% @tweets.each do |tweet| %>
    <%= render tweet %>
    <p>
      <%= link_to "Show this tweet", tweet %>
    </p>
  <% end %>
</div>

<%= link_to "New tweet", new_tweet_path %>

```

```erb:_tweet.html.erb

  <div id="<%= dom_id tweet %>" style="padding:20px 0">
    <%= tweet.content %>
  </div>
```

### ページネーションを実装する
続いてページネーションを実装します。ページネーションとは、表示すべきコンテンツが多すぎる際に一定の数で区切り、続きを見る際は「2,3,4・・・」のようなリンクを押す形のUIです。

ページネーションは`gem kaminari`で実装します。Gemfileに以下のように追記します。

```ruby:Gemfile
# 下部に追記
gem "faker"
gem 'kaminari'
```

bundle installします。

```bash:ターミナル
docker-compose run i-scroll-web bundle
```

tweets_controller.rbのindexアクションの中身を、`kaminari`の仕様にそった形に書き換えます。

```ruby:tweets_controller.rb
class TweetsController < ApplicationController
  before_action :set_tweet, only: %i[ show edit update destroy ]

  # GET /tweets
  def index
    @tweets = Tweet.order(created_at: :desc).page(params[:page]).per(40)
  end

 # 以下略
end
```

これで、一覧ページの最初の表示時には40件しかツイートが表示されないことになります。

### 無限スクロールを実装する
ここが肝です。

無限スクロールには、`Turbo Frame`を利用します。

```erb:views/tweets/index.html.erb
<div class="tweet-page" style="padding:10px 40px">
  Tweets
  <%# 今のページの`<turbo-frame>` %>
  <%= turbo_frame_tag "tweets-page-#{@tweets.current_page}" do %>
    <%# 今のページで取得したTweet一覧 %>
    <%= render @tweets %>

    <%# 遅延読み込みで次ページを取得する`<turbo-frame>` %>
    <%= turbo_frame_tag "tweets-page-#{@tweets.next_page}", loading: :lazy, src: path_to_next_page(@tweets) %>
  <% end %>
</div>
```

これだけです。

**たったのこれだけで、tweet一覧画面が無限スクロールになります。**

書く量が少なすぎる。。。

## なぜ無限スクロールになるのか

書き換えた`index.html.erb`のポイントは以下の通りです。

`turbo_frame_tag ~ do`で`<render @tweets>`を囲った
`turbo_frame_tag`の`loading: :lazy`オプションで遅延読み込みを行った

### `turbo_frame_tag ~ do ~ end`で`<render @tweets>`を囲った

`turbo_frame_tag ~ do ~ end`で囲った部分は、リクエストがTurboのリクエストとなります。

Turboのリクエストは非同期になり、ページを切り替えません。

その時リクエストするのが、次節で説明する`loading: :lazy`がついた`turbo_frame_tag`の読み込みです。

### `turbo_frame_tag`の`loading: :lazy`オプションで遅延読み込みを行った
turbo_frame_tagでこのオプションがついている場合、<turbo-frame>の要素が読み込まれたタイミングで`src`へのリクエストをします。
今回、kaminariを使ったpaginationのオプションを利用して次のページの番号を取ったり、次のページを取るためのリクエストURLを準備しています。