「なんかかっこいいから!!」【Rails7 Hotwire】を使ってSPA化した簡単なアプリを作ってみた(浅っ!!!)

# はじめに
この記事は[RUNTEQ Advent Calendar 2022](https://qiita.com/advent-calendar/2022/runteq) 14日目のものです。

はじめまして！プログラミングスクールRUNTEQにて、未経験からエンジニア転職を目指して学習中の[柴犬](https://twitter.com/taasan_runteq32)です。🐶

「これからポートフォリオを作成するぞ！！」「Hotwireって何ができるの？」となってる方々が興味を持ってくれたらいいなぁと思って書いてみました。

Rails7やHotwireについては下記の「事前知識」に軽くまとめました。必要ない方はスキップして下さい🙏
<details>
  <summary>事前知識</summary>

# Rails6から7の変更点
- **JavaScriptバンドラーの変更**
Rails6でデフォルトだった`webpacker`からRails7では`importmap-rails`を使うことになりました。

   importmap-railsによってJavaScriptのトランスパイルやビルドを必要とせず、CDN経由でライブラリを取得できるようになりました。
   つまりWebpackやYarn、npmに依存せず開発ができる。一方でReactやTypeScriptの相性は良くないみたいです:sob:

   importmapを試してみたい方は[こちら](https://www.agent-grow.com/self20percent/2021/12/09/rail7%E3%81%A7%E3%81%AEimportmap%E3%82%92%E7%B4%B9%E4%BB%8B%E3%80%90agent-grow-advent-calendar-2021%EF%BC%9A9%E6%97%A5%E7%9B%AE%E3%80%91/)が参考になります。

- **Hotwireの導入**
Hotwireを簡単に説明すると、JavaScriptをあまり使わずに**SPA**(Single Page Application)を実現するための仕組みです。

  ※その他の変更点は自身で調べてみて下さい🙇‍♂️
　 [Rails 7: importmap-rails gem README（翻訳)](https://techracho.bpsinc.jp/hachi8833/2022_06_29/112183)
　 [Rails 7.0 で標準になった importmap-rails とは何なのか？](https://zenn.dev/takeyuweb/articles/996adfac0d58fb)
# Hotwireとは？
**H**tml **O**ver **T**he **WIRE**の略です。必殺技みたいでかっこいいですね！！

ReactやVueではクライアントからのリクエストに対して、サーバー側が`JSON`を返し、それを受け取ったクライアントがDOMを構築してHTMLをレンダリングします。

一方、Hotwireではクライアントからのリクエストに対して、サーバー側が`HTML`を返すので、クライアント側でレンダリングする必要はありません。

つまり、開発者は**サーバーサイドのみ**に集中できます。🙌

Hotwireは以下の技術の総称です。(※今回は`Turbo`だけ使いました。)
- Turbo(Web開発向け)
  - Turbo Drive
  - Turbo Frames
  - Turbo Streams
  - Turbo Native(モバイルアプリ開発向け)
- Stimulus(Web開発向け)
- Strada(モバイルアプリ開発向け)

※詳細は割愛します。気になる方はこちら
 [Hotwireとは？](https://zenn.dev/shita1112/books/cat-hotwire-turbo/viewer/abstract)
 [Hotwireとは何なのか？](https://zenn.dev/en30/articles/2e8e0c55c128e0)
</details>

# 作ったもの
※[RUNTEQ Advent Calendar 2022 12/7日担当の方の記事](https://qiita.com/Tsuchiy_2/items/73d529ff820a1eed58e0)
とテーマが被ってしまった(いや、ほんと偶然です笑)ため、一部割愛します。

以下の機能をもった画像投稿アプリ

・投稿の編集
[![Image from Gyazo](https://i.gyazo.com/070fceece65d07b414148c100315e740.gif)](https://gyazo.com/070fceece65d07b414148c100315e740)
・投稿の削除
[![Image from Gyazo](https://i.gyazo.com/63c8a78b1f9ffb613f73c8c74cfe7bf9.gif)](https://gyazo.com/63c8a78b1f9ffb613f73c8c74cfe7bf9)
・ページネーション(無限スクロール)
[![Image from Gyazo](https://i.gyazo.com/182368411c9e52e11a4c241f97d1b9f5.gif)](https://gyazo.com/182368411c9e52e11a4c241f97d1b9f5)
・検索機能
[![Image from Gyazo](https://i.gyazo.com/54241ed376edc2d74b4d2364e1cdda87.gif)](https://gyazo.com/54241ed376edc2d74b4d2364e1cdda87)

## 開発環境・使用技術
・M1 macbookAir 2020
・ruby3.1.0　　　　　　　　　　　　
・rails 7.0.4
・esbuild
・node 16.18.1
・yarn 1.3.2
・tailwind css
・daisyUI

<details><summary>セットアップ(ほぼ自分用のメモです🙇‍♂️)</summary>

rails new
```zsh
rails new post-hotwire -c tailwind -j esbuild -d postgresql -skip-jbuilder –skip-action-mailbox –skip-action-text -MCT
```

<details><summary>rails new時のオプション</summary>

`-j esbuild`
importmapではdaisyUIが使えません。(2022/12/14時点)
webpacker(shakapacker)やesbuildを使う必要があるのですが、今回は後者を使いました。(脱Node.jsならず:cry:)

JavaScriptバンドラーを指定せずに、`-c tailwind`とするとデフォルトでimportmap-railsが使われます。
今回はesbuildを指定しているので`cssbuilding-rails`経由でtailwind cssをインストールしています。
cssを指定せずにrails new後、`tailwindcss-rails`をインストールするとscaffold実行時に勝手にレイアウトを整えてくれるのでより簡単です。

`-C`
Turbo Streams(WebSocket)を使う際は内部的にRedisとAction Cableを使うので注意しましょう。

参考
[Rails 7 : rails newのフロントエンド関連オプションの組み合わせを調べてみた](https://techracho.bpsinc.jp/hachi8833/2021_12_28/114730)
</details>

DB作成
```zsh
bin/rails db:create
Created database 'post-hotwire_development'
Created database 'post-hotwire_test'
```
tailwindの公式プラグインをインストール
```zsh
yarn add @tailwindcss/typography @tailwindcss/forms @tailwindcss/line-clamp @tailwindcss/aspect-ratio
```
daisyuiをインストール
```zsh
yarn add daisyui
```
tailwindの設定を追加
```tailwind.config.js
module.exports = {
  content: [
    './app/views/**/*.html.erb',
    './app/helpers/**/*.rb',
    './app/assets/stylesheets/**/*.css',
    './app/javascript/**/*.js'
  ],
  // 以下を追記
  plugins: [
    require('@tailwindcss/forms'),
    require('@tailwindcss/line-clamp'),
    require('@tailwindcss/aspect-ratio'),
    require('@tailwindcss/typography'),
    require("daisyui")
  ]
}
```
</details>

# 各機能の説明
さて、ここからが本番です。(前置きが長い？アーニャさんのように大人の余裕を持ちましょう！)
各機能でURLが変わっていないことにも注目してくださいね！🔎
## 1. 投稿の編集
[![Image from Gyazo](https://i.gyazo.com/070fceece65d07b414148c100315e740.gif)](https://gyazo.com/070fceece65d07b414148c100315e740)
```ruby:app/controllers/posts_controller.rb
  # 一部省略
  def edit; end

  def update
    if @post.update(post_params)
      flash.now.notice = '投稿を更新しました。'
    else
      flash.now.alert = '投稿を更新できませんでした。'
      render :edit, status: :unprocessable_entity
    end
  end

```
if節がtrueのとき、Railsがよしなに`update.turbo_stream.erb`を`render`してくれます。

```erb:update.turbo_stream.erb
<%= turbo_stream.replace @post %>
<%= turbo_stream_flash %>
```

上記を省略せずに書くと
```erb:update.turbo_stream.erb
<%= turbo_stream.replace dom_id(@post) do %>  <%#= dom_id(@post)で"post_○"を指す =%>
  <%= render partial: "posts/post", locals: { payment: @payment } %>
<% end %>
<%= turbo_stream_flash %>
```
こうなります。Railsのよしな力高すぎんか？
`replace`でid="post_○"が一致するものを書き換えます。

```erb:app/views/posts/_post.html.erb
<%# 長くなるので省略 %>
<%= turbo_frame_tag post do %>
  <%= image_tag post.image %>
  <%= post.title %>
  <%= post.content %>
<% end %>
```
```html
<!-- 上記コードで生成されるHTML -->
<turbo_frame id="post_○">
  <img src="○○">
  タイトル
  内容
</turbo_frame>
```
`turbo_stream_flash`はフラッシュメッセージを表示する独自メソッドとして実装しました。
```app/helpers/application_helper.rb
module ApplicationHelper
  def turbo_stream_flash
    turbo_stream.update 'flash', partial: 'shared/flash'
  end
end
```

## 2. 投稿の削除

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

```app/controllers/posts_controller.rb
  def destroy
    @post.destroy!
    flash.now.notice = '投稿を削除しました。'
  end
```
```erb:app/views/posts/destroy.turbo_stream.erb
<%= turbo_stream.remove @post %>
<%= turbo_stream_flash %>
```
理屈は編集のときと同じです。
`remove`で指定したidがつけられている`turb_frameタグ`で囲まれた投稿を削除します。

※ちなみに新規投稿の際は同じ理屈でprependメソッドを使いました。

## 3. ページネーション(無限スクロール)
[![Image from Gyazo](https://i.gyazo.com/182368411c9e52e11a4c241f97d1b9f5.gif)](https://gyazo.com/182368411c9e52e11a4c241f97d1b9f5)

gem `kaminari`を使って実装しました。
```erb:app/views/posts/index.html.erb
<%= turbo_frame_tag "posts-page-#{@posts.current_page}" do %>
  <% if @search_posts %>
    <%= render @search_posts %>    
  <% else %>
    <%= render @posts %>
  <% end %>
  <%= turbo_frame_tag "posts-page-#{@posts.next_page}", loading: :lazy, src: path_to_next_page(@posts) %>
<% end %>
```
`path_to_nex_page(@posts)`はkaminariのメソッドで`/posts2/`など次のページのパスを返します。
`src`属性を指定すると、/posts2/のページロード時に<turbo-frame id="posts-page-2">にマッチした部分を置き換えます。
さらに`loading`オプションを`lazy`にするとページロード時ではなく<turbo-frame>タグがページに現れたときに読み込むようになります。
以上の流れで無限スクロールを実装しています。(難しい…)
https://techracho.bpsinc.jp/hachi8833/2021_09_17/111148
https://turbo.hotwired.dev/reference/frames#lazy-loaded-frame

## 4. 検索
[![Image from Gyazo](https://i.gyazo.com/54241ed376edc2d74b4d2364e1cdda87.gif)](https://gyazo.com/54241ed376edc2d74b4d2364e1cdda87)

gem `ransack`を使って実装しました。
```app/controllers/application_controller.rb
class ApplicationController < ActionController::Base
  before_action :set_search

  def set_search
    @search = Post.ransack(params[:q])
    @search_posts = @search.result.order(created_at: :desc).page(params[:page]).per(12)
  end
end
```
```erb:app/views/shared/_header.html.erb
<%# 一部抜粋&省略 %>
<%= search_form_for @search, html: { data: { turbo_frame: "posts-list" } }, url: posts_path do |f| %>
  <%= f.search_field :title_or_content_cont, placeholder: "検索ワード" ,class: "input input-bordered" %>
  <%= f.submit class: "btn btn-ghost btn-circle" %>
<% end %>
```
```erb:index.html.erb
<%= turbo_frame_tag "posts-list" do %>
  <%#　全てのコードをturbo_frame_tagで囲む %>
<% end %>
```
index.html.erb内のコードは全て`turbo_frame_tag`で囲み、id="posts-list"としているので、検索を実行するとindex.html全てが検索結果の投稿に置き換わります。


# 最後に
今回は「なんかかっこいいから！！」全部SPAにしてみました。(浅っ！！！)
実際には開発スピード重視で一部にHotwireを導入する使い方で良いと思います。

VueやReactほど細かいことはできませんが、Hotwierを使えばある程度同じ機能は簡単に実現できそうです。
これこそRailsの思想に基づいたフロントエンド開発って感じがして個人的に好きです。笑

「バックエンドはRails、フロントエンドはReact(Next.js)でSPA作ってやるぜ！！」「だけど、なるべく時間をかけたくない…」「開発効率もUI/UXも重視ししたい…」

そこのあなた、Hotwireを使って快適なRailsライフを送ってみませんか？