[Rails]Stimulus Content Loaderを使ってあとからコンテンツを読み込む

## はじめに

Railsでトップページや管理画面のダッシュボードなど、表示するコンテンツが多いページを作るときに1つのコントローラーで全てを取得するように作ると物によっては遅くなってしまうのではないでしょうか。
対策としてページ表示後にコンテンツを読み込むように `Stimulus`を使って実装してみようと思います。
こういった手法を`遅延読み込み`や`遅延ロード`と言ったりします。英語だと`lazy load`です。
よく画像の表示で使われたりします。

## デモ

[![Image from Gyazo](https://i.gyazo.com/af3a7ca8025680f01ccd85a78134cf38.gif)](https://gyazo.com/af3a7ca8025680f01ccd85a78134cf38)

ソースコードは[こちら](https://github.com/harayama-developmer/rails-stimulus-content-loader-sandbox)

## 実装

### 環境

- Rails 6.1.3.1
- Ruby 2.7.2
- Yarn 1.22.5
- Node 14
- Docker

### 流れ

`!!!注意!!!`
`今回Dockerを使っていますが、ここではDockerについては割愛しています。なのでdocker-composeなどのコマンドを省略しています。実際に再現する場合は各自の環境に合わせてコマンドや設定を変更してください。`

今回Stimulusを使うのでwebpackでインストールします。

```bash
rails new my_app --webpack=stimulus
```

localhost:3000を確認します。

```bash
rails s
```

![Ruby_on_Rails.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/100766/e2b7ac88-cfef-7416-d2ef-03267eb6f99f.png)

この時点で`app/javascript`配下に作成されたファイルを確認します。
rails newするときに`--webpack=stimulus`を指定したのでStimulusのcontrollerを読み込む設定が作成されています。

```js:app/javascript/packs/application.js
import Rails from "@rails/ujs"
import Turbolinks from "turbolinks"
import * as ActiveStorage from "@rails/activestorage"
import "channels"

Rails.start()
Turbolinks.start()
ActiveStorage.start()

import "controllers" // これ
```

```js:app/javascript/controllers/index.js
import { Application } from "stimulus"
import { definitionsFromContext } from "stimulus/webpack-helpers"

const application = Application.start()
const context = require.context("controllers", true, /_controller\.js$/)
application.load(definitionsFromContext(context))
```

```js:app/javascript/controllers/hello_controller.js
import { Controller } from "stimulus"

export default class extends Controller {
  static targets = [ "output" ]

  connect() {
    this.outputTarget.textContent = 'Hello, Stimulus!'
  }
}
```

まずはCRUD機能を作成していきます。
デモにあるように、今回は投稿とコメントの2つを作成して、投稿詳細ページを表示後にコメントを読み込むといった形を実装してみます。

```bash
$ rails g scaffold posts title body:text
$ rails g model comment post:references body:text
$ rails db:migrate
```

投稿詳細ページでコメントを閲覧・作成できるように変更します。

```rb:config/routes.rb
Rails.application.routes.draw do
  resources :posts do
    resources :comments, only: %i(index create destroy)
  end
end
```

```rb:app/models/post.rb
class Post < ApplicationRecord
  has_many :comments, dependent: :destroy
end
```

```rb:app/controllers/comments_controller.rb
class CommentsController < ApplicationController
  before_action :set_post

  def create
    @comment = @post.comments.new(comment_params)

    respond_to do |format|
      if @comment.save
        format.html { redirect_to @post, notice: "コメントしました" }
        format.json { render :show, status: :created, location: @post }
      else
        format.html { render :new, status: :unprocessable_entity }
        format.json { render json: @comment.errors, status: :unprocessable_entity }
      end
    end
  end

  def destroy
    @comment = @post.comments.find(params[:id])

    @comment.destroy
    respond_to do |format|
      format.html { redirect_to @post, notice: "コメントを削除しました" }
      format.json { head :no_content }
    end
  end

  private

  def set_post
    @post = Post.find(params[:post_id])
  end

  def comment_params
    params.require(:comment).permit(:body)
  end
end
```

```slim:app/views/posts/show.html.slim
p#notice = notice

p
  strong タイトル:
  p = @post.title
p
  strong 本文:
  = simple_format @post.body

=> link_to 'Edit', edit_post_path(@post)
'|
=< link_to 'Back', posts_path

/ 以下を追加

h4 コメント

- if @comments.present?
  ul
    - @comments.each do |comment|
      li
        = simple_format comment.body
        span = link_to "削除", [@post, comment], data: { confirm: 'Are you sure?' }, method: :delete
- else
  p コメントはありません。

hr

= form_with model: [@post, Comment.new] do |f|
  .field
    = f.label :body, "本文"
    br
    = f.text_area :body, required: true
  .actions = f.submit
```

ここまでで、投稿詳細ページでコメントできるようになりました。
ただ、コメントはページ時に取得しているので、これをページ表示後に取得するようにStimulusを使っていきます。

JSを書いていってもいいですが、以下のcomponentを使えばさくっと実装できそうなのでこれを使ってみます。

https://stimulus-components.netlify.app/docs/components/stimulus-content-loader/

```bash
$ yarn add stimulus-content-loader
```

次にimportします。

```js:app/javascript/controllers/index.js
import { Application } from "stimulus"
import { definitionsFromContext } from "stimulus/webpack-helpers"
import ContentLoader from "stimulus-content-loader" // 追加

const application = Application.start()
const context = require.context("controllers", true, /_controller\.js$/)
application.load(definitionsFromContext(context))
application.register("content-loader", ContentLoader) // 追加
```

コントローラーを変更します。
`stimulus-content-loader`ではページ表示後に非同期でコンテンツを取得するので、実行したいアクションを追加します。

```rb:app/controllers/comments_controller.rb
  # 省略...

  # 追加
  def index
    render partial: 'posts/comments', locals: { post: @post, comments: @post.comments }
  end

  # 省略...
```

```slim:app/views/posts/_comments.html.slim
- if comments.present?
  ul
    - comments.each do |comment|
      li
        = simple_format comment.body
        span = link_to "削除", [post, comment], data: { confirm: 'Are you sure?' }, method: :delete
- else
  p コメントはありません。
```

```slim:app/views/posts/show.html.slim
/ 省略...

h4 コメント
div[data-controller="content-loader" data-content-loader-url-value="#{ post_comments_path(@post) }"]
  p ローディング中...

/ 省略...
```

これでOKです。
実行してみるとデモのような動きになるはずです。

ログを確認してみます。
最初に投稿詳細ページを表示したあとに、コメントを取得しているのが分かります。

![1__docker-compose_up.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/100766/49de52af-b28b-95a1-c65a-7bb42cf4ac81.png)

以上で完成です。
[Stimulus Content Loader](https://stimulus-components.netlify.app/docs/components/stimulus-content-loader/)が便利でJSを書かなくても実装できました。

よかったら試してみて下さい。

## 補足

- Stimulus Content Loaderには`data-content-loader-refresh-interval-value`を指定することで、表示したコンテンツをN秒ごとに再ロードする設定ができるようです。
- ソースコードも公開されているのでJSの勉強にも使えそうです。今回のStimulus Content Loaderのコードは[こちら](https://github.com/stimulus-components/stimulus-content-loader/blob/master/src/index.js)
- [Stimulus Components](https://stimulus-components.netlify.app/docs/components/index/)には、他にも多くのcomponentがあるので便利そうです。