Turbo Stream × Action Cable でリアルタイム更新を実装する！Turbo Rails Tutorial をやってみた（5章）


本記事は、🔽「Turbo Rails Tutorial」の学習を進めていく際の個人メモです！

https://www.hotrails.dev/turbo-rails

🔽 前章の記事はこちら

https://qiita.com/yuppymam/items/4fa3ce68250c8038b77b

# 5章 [Real-time updates with Turbo Streams](https://www.hotrails.dev/turbo-rails/turbo-streams)

何やら Turbo Stream と Action Cable を使うと、リアルタイム更新の実装はめちゃくちゃ簡単で、コードもほとんど必要ないとの事。試してみましょう。

## リアルタイム更新（`create`）を実装する
※`redis`入っていない場合は`brew install redis`しておきます。
```terminal
$ bin/dev
```
で、`redis`が動いてなさそうなので、`Procfile.dev`に`redisーserver`を追加してやり直し 🔽
```Procfile.dev
web: unset PORT && bin/rails server
js: yarn build --watch
css: yarn build:css --watch
redis: redis-server /usr/local/etc/redis.conf
```
まずは`Quote`モデルに以下のコードを追加します。

```app/models/quote.rb
class Quote < ApplicationRecord
  validates :name, presence: true

  scope :ordered, -> { order(id: :desc) }

  # 以下を追加する
  after_create_commit -> { broadcast_prepend_to "quotes", partial: "quotes/quote", locals: { quote: self }, target: "quotes" }
end
```
次にビュー側に`quotes`の stream を受け取るコードを書きます。

```app/views/quotes/index.html.erb
<%= turbo_stream_from "quotes" %>
.
.
```

たったこれだけです、リアルタイム更新を確認してみます。
### 確認方法1. コンソールを使う
`rails console`から新しい見積もりを作成してみます。
```console
> Quote.create!(name: "コンソールから作成")
```
ブラウザを見てみると、即座に追加されています。
![スクリーンショット 2023-04-29 11.11.11.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/293962/46c57dd0-0887-d6f2-68de-c494555a98ee.png)

### 確認方法2. 複数のウインドウを並べる
ブラウザを２つ並べてリアルタイム更新を確認できます。右で作成したものが左にも即座に反映されていますね！
![output.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/293962/e2d027ea-0334-1e83-2985-180fc5d38a6d.gif)

### リファクタリング
先ほどモデルに書いたコードは、短縮が可能です。まずは、現在のフルバージョンがこちら。
```app/models/quote.rb
after_create_commit -> { broadcast_prepend_to "quotes", partial: "quotes/quote", locals: { quote: self }, target: "quotes" }
```
最後にターゲット名を`"quotes"`と指定していますが、デフォルトではモデル名の複数形がターゲットに入るので`target: "quotes"`を省略できます。

```app/models/quote.rb
after_create_commit -> { broadcast_prepend_to "quotes", partial: "quotes/quote", locals: { quote: self } }
```
また、`partial`と`locals`にもデフォルト値が設定されています。

- パーシャルのデフォルト値は、`Quote`モデルでは`"quotes/quote"`
- `locals`のデフォルト値は`{ モデル名をシンボル化したもの => self }`で、`Quote`モデルでは`{ quote: self }`

デフォルト値と現在記載しているものは同等なので、これらも省略可能となり、最終的には以下のコードになりました。

```app/models/quote.rb
after_create_commit -> { broadcast_prepend_to "quotes" }
```

## リアルタイム更新（`update`）を実装する

`Quote`モデルにコードを追加します。
```app/models/quote.rb
class Quote < ApplicationRecord
  validates :name, presence: true

  scope :ordered, -> { order(id: :desc) }

  after_create_commit -> { broadcast_prepend_to "quotes" }
  # 以下を追加する
  after_update_commit -> { broadcast_replace_to "quotes" }
end
```

コードを追加したら一旦`rails console`を再起動し、`create`の時と同じくコンソールから更新すると、
```console
> Quote.last.update!(name: "コンソールから更新")
```
ブラウザで更新されています。
![スクリーンショット 2023-04-29 13.36.02.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/293962/fd0ac747-f258-5351-4d00-c6c577ea308c.png)

## リアルタイム更新（`destroy`）を実装する
この雰囲気で進めてきたら、`destroy`も`Quote`モデルに１行足すだけで実装できそうだなと想像できますね。
```app/models/quote.rb
class Quote < ApplicationRecord
  validates :name, presence: true

  scope :ordered, -> { order(id: :desc) }

  after_create_commit -> { broadcast_prepend_to "quotes" }
  after_update_commit -> { broadcast_replace_to "quotes" }
  # 以下を追加する
  after_destroy_commit -> { broadcast_remove_to "quotes" }
end
```

`rails console`を再起動しコンソールから削除すると、
```console
> Quote.last.destroy!
```
最後の見積もりが削除されています。
![スクリーンショット 2023-04-29 13.45.03.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/293962/9a875158-61c3-44af-f51d-64bf44ed12bd.png)

## ActiveJob で非同期にする
これまで実装してきたリアルタイム更新は、非同期処理することでさらにパフォーマンスを向上させることが可能です。Turbo Streamsの画面の更新は、非同期処理で行われることが推奨されています。

```app/models/quote.rb
class Quote < ApplicationRecord
  validates :name, presence: true

  scope :ordered, -> { order(id: :desc) }

  # 以下に変更
  after_create_commit -> { broadcast_prepend_later_to "quotes" }
  after_update_commit -> { broadcast_replace_later_to "quotes" }
  after_destroy_commit -> { broadcast_remove_to "quotes" }
end
```

※`broadcast_remove_later_to`は存在しないようです。（データベースから削除されてしまうとJob実行時にレコードを取得できないから）

コンソールから見積もりを作成してみると、ログが`from Async`となっているのを確認できました。
```console
> Quote.create!(name: "Asynchronous quote")
.
.
Performing Turbo::Streams::ActionBroadcastJob (Job ID: 96977e78-e5a5-4308-9ca7-3a3838f57907) from Async(default) enqueued at 2023-04-29T11:54:42Z with arguments: "quotes", {:action=>:prepend, :target=>"quotes", :targets=>nil, :locals=>{:quote=>#<GlobalID:0x000000010cf3db28 @uri=#<URI::GID gid://quote-editor/Quote/908005744>>}, :partial=>"quotes/quote"}
  Rendered quotes/_quote.html.erb (Duration: 0.4ms | Allocations: 297)
[ActionCable] Broadcasting to quotes: "<turbo-stream action=\"prepend\" target=\"quotes\"><template><turbo-frame id=\"quote_908005744\">\n  <div class=\"quote\">\n    <a data-turbo-frame=\"_top\" href=\"/quotes/908005744\">Asynchronous quote</a>\n    <div class=\"quote__actions\">\n      <form class=\"button_to\" method=\"post\" acti...
Performed Turbo::Streams::ActionBroadcastJob (Job ID: 96977e78-e5a5-4308-9ca7-3a3838f57907) from Async(default) in 2.78ms
```

### リファクタリング

ここまでで、リアルタイム更新を非同期で行うことができました。そして今から、RailsのDRY魂を見ることになります。

機能はそのままでこうなります。
```app/models/quote.rb
class Quote < ApplicationRecord
  validates :name, presence: true

  scope :ordered, -> { order(id: :desc) }

  # after_create_commit -> { broadcast_prepend_later_to "quotes" }
  # after_update_commit -> { broadcast_replace_later_to "quotes" }
  # after_destroy_commit -> { broadcast_remove_to "quotes" }
  # 上記の3行は、次の１行と同等
  broadcasts_to ->(quote) { "quotes" }, inserts_by: :prepend
end
```

## まとめ
（ビューとモデルに1行ずつ、計2行追加するだけで、リアルタイム更新が完成した。Railsすっご！）

特にモデルに追加した1行は、徐々にリファクタリングしていったので理解が深まった気がします。

次の章では、セキュリティについて学びます。Turbo Streams を安全に使用し、非同期処理の画面更新時に間違ったユーザーに送信しないようにする方法について学びます。

https://www.hotrails.dev/turbo-rails/turbo-streams-security