シンプルな例で体感するHotwire (1)Turbo FramesでHTMLの部分更新

Hotwireにはいくつかの機能がありますが、まずはとても効果がわかりやすいところとしてTurbo FramesでHTMLを部分更新できるので、これを見てみましょう。

- Railsでの用例の話をします
- 記事の読者はRailsの基礎は知っているものとします
- Rails7.0.4 + Ruby3.1.2 で動作させています

## おおまかな流れ

まずは概要を頭に入れて、流れをイメージしやすくしましょう。

以下は、

- 基本的なRailsアプリ
- Rails+JS（Reactなど）アプリ
- Rails+Turbo Framesアプリ

の3つの、処理の違いをシーケンス図に表したものです。（PlantUMLで書きました）

```plantuml
@startuml
participant "Rails Server"
participant Browser

group 基本的なRailsアプリの典型的な動作
  Browser -> "Rails Server": リクエストを送る
  activate "Rails Server"
  "Rails Server" -> Browser: ページのHTMLを含むレスポンスを返す
  deactivate "Rails Server"
  Browser -> Browser: ページを表示する
  activate Browser
  Browser -> "Rails Server": 次のリクエストを送る
  activate "Rails Server"
  "Rails Server" -> Browser: 次のページのHTMLを含むレスポンスを返す
  deactivate "Rails Server"
  deactivate Browser
  Browser -> Browser: 次のページを表示する
  activate Browser
  deactivate Browser
end

group Rails+JS(Reactなど)アプリの典型的な動作
  Browser -> "Rails Server": リクエストを送る
  activate "Rails Server"
  "Rails Server" -> Browser: ページのHTMLを含むレスポンスを返す
  deactivate "Rails Server"
  Browser -> Browser: ページを表示する
  activate Browser
  Browser -> "Rails Server": APIリクエストを送る
  activate "Rails Server"
  "Rails Server" -> Browser: JSONを含むレスポンスを返す
  deactivate "Rails Server"
  Browser -> Browser: JS(Reactなど)がページを変更する
end
deactivate Browser

group Rails+Turbo Framesアプリの典型的な動作
  Browser -> "Rails Server": リクエストを送る
  activate "Rails Server"
  "Rails Server" -> Browser: ページのHTMLを含むレスポンスを返す
  deactivate "Rails Server"
  Browser -> Browser: ページを表示する
  activate Browser
  Browser -> "Rails Server": リクエストを送る
  activate "Rails Server"
  "Rails Server" -> Browser: ページの一部のHTMLを含むレスポンスを返す
  deactivate "Rails Server"
  Browser -> Browser: Turbo Framesがページの一部をレスポンスのHTMLに置き換える
end
deactivate Browser

@enduml
```

Turbo Framesの重要なポイントは

- 基本的なRailsアプリと違い、HTMLの必要な部分だけを変更するため、ページ全体を切り替えるよりブラウザの処理が軽くなる
- Rails+JS（Reactなど）アプリと違い、Rails server側でHTMLを作るため、JSの記述をとても少なくできる（基本的な機能だけならゼロにできる）
  - HTMLを作るのはRails側!というまとまった状態を維持できる、という事でもあります

というところだと思います。


## 実装

部分更新をする例です。「おみくじ機能があるダッシュボード」を考えてみます。

```app/controllers/dashboard_controller.rb
class DashboardController < ApplicationController
  # root "dashboard#index"
  def index
  end

  # post 'fortune', to: 'dashboard#fortune'
  def fortune
    @fortune = %w(大吉 中吉 小吉 凶).sample
    render partial: 'fortune'
  end
end
```

```app/views/dashboard/index.html.erb
<p>こんにちは</p>

<%= turbo_frame_tag 'fortune' do %>
  <%= form_with url: fortune_path do |f| %>
    <%= f.submit 'おみくじを引く！' %>
  <% end %>
<% end %>
```

```app/views/dashboard/_fortune.html.erb
<%= turbo_frame_tag 'fortune' do %>
  <p>今日の運勢は<%= @fortune %>です</p>

  <%= form_with url: fortune_path do |f| %>
    <%= f.submit '引き直す！' %>
  <% end %>
<% end %>
```

rails newしてこれらのコード（とroutes.rb）を書くだけでTurbo Framesを早速動作させることができます。

ここで重要な事は2つ、

- turbo_frame_tag に引数で渡した名前が一致する箇所を部分更新する
- controller上で`render partial: 'xxx'`と書くとpartialの内容だけをレスポンスに返す

です。

## 動作

![1](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/17353/f6727c2b-22bb-1b54-0d5c-89b1f21a5941.png)

これが、

![2](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/17353/c0ce9d55-7453-be6c-2d2a-20f73251e056.png)

こうなります。

例がシンプルすぎて「普通じゃん」ってなってしまいそうなので、何が起きているのかもう少し見ていきましょう。

最初のページのHTMLは以下のようになっています。

```html
<!DOCTYPE html>
<html>
  <head>
    （省略）
  </head>

  <body>
    <p>こんにちは</p>

    <turbo-frame id="fortune">
      <form action="/fortune" accept-charset="UTF-8" method="post">
        <input type="hidden" name="authenticity_token" value="U2VGLjfze2oiCQI5M6dDbGV4SoOZ7UPVg1QILztHNFd7pL51Pg8VnAFcbL5F_cyeitHzT_tmIu55eZW_66fvBg" autocomplete="off" />
        <input type="submit" name="commit" value="おみくじを引く！" data-disable-with="おみくじを引く！" />
      </form>
    </turbo-frame>
  </body>
</html>
```

erbファイルにturbo_frame_tagを書いた部分は、HTML上ではturbo-frameタグになっています。

次に、おみくじを引くリクエストのレスポンスのHTMLは以下のようになっています。

```html
<turbo-frame id="fortune">
  <p>今日の運勢は凶です</p>

  <form action="/fortune" accept-charset="UTF-8" method="post">
    <input type="hidden" name="authenticity_token" value="tQdPBfoZP7j2pXfTEM7_njNTeaFxFNriKhqVFhpgNr2dxrde8-VRTtXwGVRmlHBs3PrAbROfu9nQNwiGyoDt7A" autocomplete="off" />
    <input type="submit" name="commit" value="引き直す！" data-disable-with="引き直す！" />
  </form>
</turbo-frame>
```

抜粋したのではなくて、これで全部です。

turbo-frameタグのidが同じで、Turbo Framesがこの内容を見て、元のページのturbo-frameタグの内容を置き換えます。
それでおみくじの結果が画面に出る、という流れです。

ここまで見ておわかりかと思いますが、Turbo Framesは「こういうHTMLを返してくれるサーバーだったらRailsじゃなくても別に良い」んですよね。
独立したJSプロダクトで、Railsはあくまで連携するためのサポート機能を持っているだけです。

## もっと便利に使う

ここまでの例は、基本の原理がわかりやすいように最初のページとおみくじの結果を分けていました。

実際にアプリを作るときはもっと便利に書けるので、それもやってみましょう。

erbファイルを修正します。

```app/views/dashboard/index.html.erb
<p>こんにちは</p>

<%= render partial: 'fortune' %>
```

```app/views/dashboard/_fortune.html.erb
<%= turbo_frame_tag 'fortune' do %>
  <% if @fortune %>
    <p>今日の運勢は<%= @fortune %>です</p>

    <%= form_with url: fortune_path do |f| %>
      <%= f.submit '引き直す！' %>
    <% end %>
  <% else %>
    <%= form_with url: fortune_path do |f| %>
      <%= f.submit 'おみくじを引く！' %>
    <% end %>
  <% end %>
<% end %>
```

このように、最初のページと更新時で同じpartialを使うように書けます。
もっとシンプルに、最初のページを開いたときにおみくじの結果を表示して良ければ、

```app/controllers/dashboard_controller.rb
class DashboardController < ApplicationController
  # root "dashboard#index"
  def index
    @fortune = %w(大吉 中吉 小吉 凶).sample
  end

  # post 'fortune', to: 'dashboard#fortune'
  def fortune
    @fortune = %w(大吉 中吉 小吉 凶).sample
    render partial: 'fortune'
  end
end
```

```app/views/dashboard/_fortune.html.erb
<%= turbo_frame_tag 'fortune' do %>
  <p>今日の運勢は<%= @fortune %>です</p>

  <%= form_with url: fortune_path do |f| %>
    <%= f.submit '引き直す！' %>
  <% end %>
<% end %>
```

と、partialの内容を全く同じにしてしまう事もできます。ここまで来ると、RailsとTurbo Framesの連携がなかなか衝撃的に感じられるんじゃないでしょうか。

## 終わり

Hotwireの便利さを体感できるシンプルな例を出してみました。私もまだ触り始めたばかりで、色々と資料をあたって断片的な情報を結びつけてここに至った感じです。基本的な仕組みがわかりやすいシンプルな例があると良いなと思ったのでまとめてみました。

次はTurbo Streamsについて~~書きます~~[書きました](https://qiita.com/shinsuke-kuroki/items/e47821d62567bac4e640)。

## 参考にした記事

- [HotwireはRailsを「ゼロJavaScript」でリアクティブにできるか？前編（翻訳）](https://techracho.bpsinc.jp/hachi8833/2021_06_09/108495)
- [Turbo Handbook](https://turbo.hotwired.dev/handbook/introduction) 公式です
- [Hotwireとは何なのか？](https://zenn.dev/en30/articles/2e8e0c55c128e0)