フロントもRailsで！バックエンドエンジニアのHotwire体験記


今年もアドベントカレンダーの季節がやってきました！⛄️

[リンクアンドモチベーション Advent Calendar 2024](https://qiita.com/advent-calendar/2024/lmi)の1日目を担当します:christmas_tree:

# はじめに

私は普段の業務で、バックエンドをRails、フロントエンドをVue.jsで開発されたAPIモードのRailsアプリケーション開発に携わっています。

現代のWeb開発では一般的な構成ですが、最近Railsのバージョンアップが進む中で、Railsのフロントエンド開発もどんどん進化していることはご存知でしょうか？

きっかけは、10月に開催された [Kaigi on Rails](https://kaigionrails.org/2024/)での2つの印象的な発表です。

Haruna Tsujitaさんの「Hotwire or React ?」 

https://speakerdeck.com/harunatsujita/hotwire-or-react 

大場寧子さんの「Hotwire光の道とStimulus」

https://speakerdeck.com/nay3/hotwireguang-nodao-tostimulus

これらの発表を聞いて、[Hotwire](https://hotwired.dev/)に興味を持ちました。

Rails 7.0から導入されたHotwireを使えば、APIモードとは異なるアプローチで、Railsの通常モードを活用して効率よくフロントエンド開発を完結させることができます。

この記事では、普段APIモードで開発している私の視点から、Hotwireを使ったRailsフロントエンド開発の魅力をお伝えできればと思います。

また、実際のコード例も交えながら説明しますので、「Hotwireって何？」という方もぜひ読んでみてください！

# Hotwireとは？
私自身、そもそも[Hotwire](https://hotwired.dev/)が何者かわからず困ったので簡単に説明します。

Hotwireは「HTML Over The Wire」の略で、Turbolinksに代わって、Rails 7.0から標準で搭載された新しいツールです。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/614957/f0466b4e-b2a3-4fdf-1a89-bc42d6986550.png)
引用: [How to Use Hotwire Rails: Getting Started Tutorial
](https://www.bacancytechnology.com/blog/hotwire-rails-tutorial)

- **Turbo**: ページ全体のリロードを避けて、SPAのようなスムーズな読み込みを行える
- **Stimulus**: Turboと組み合わせることで、リッチなUIを構築ができる
- **Strada**: モバイルアプリやデスクトップアプリとの連携を強化できる

の3つのコンポーネントで成り立っています。

# HotwireとSPA
Hotwireをgoogleで検索すると、
「ページ全体を再読み込みせずにSPAのような体験ができる」
というような内容が書かれています。

**SPAっぽいけど、SPAではないってどういうこと？**
と疑問に思ったので、次はSPAについて少し掘り下げてみます。

## そもそもSPAとは？

昔々、Webアプリケーションはもっとシンプルでした。

リンクをクリックすると、新しいページが読み込まれて、「グルグル…」とローディングを待つ。そんな時代が当たり前だった。。。（らしい）

でも、Gmailを開いてみると、メールを開いても画面全体がリロードされることはなく、一瞬で内容が表示されます。

これがSPA（Single Page Application）です。

## HotwireとSPAのアプリケーションの違い

HotwireとSPAのアプリケーションは、どちらも「ページの一部分を更新してスムーズなユーザー体験を提供する」という目的は同じですが、その実現方法が大きく異なります。

SPAは、JavaScriptフレームワーク（Vue.jsやReactなど）を使用して、フロントエンドで**必要なデータだけをJSON形式**でAPIから取得し、クライアントサイドでHTMLをレンダリングします。

つまり、画面の更新に必要なロジックのほとんどがフロントエンドに存在することになります。


一方、Hotwireは「HTML over the Wire」という名前の通り、サーバーサイドでレンダリングしたHTMLの**必要な部分だけをワイヤー（通信）** で送信します。

これにより、JavaScriptを最小限に抑えつつ、Railsの伝統的な開発方法を活かしたまま、SPAのような体験を実現できます。

```mermaid
graph LR
    User[ユーザーアクション]

    subgraph "SPAで実装"
        Frontend[フロントエンドVue.js/React]
        API[APIエンドポイント]
        Backend[バックエンド処理]
    end

    subgraph "Hotwireで実装"
        HForm[Railsフォーム]
        HBackend[バックエンド処理+ HTML生成]
    end

    %% SPAのフロー
    User --> Frontend
    Frontend --> API
    API --> Backend
    Backend --> API
    API --> Frontend
    Frontend --> Update1[JavaScriptでDOM操作]

    %% Hotwireのフロー
    User --> HForm
    HForm --> HBackend
    HBackend --> Update2[必要なHTML部分のみ]

    style Frontend fill:#FFE4E1
    style HForm fill:#E6E6FA
    style Backend fill:#E0FFFF
    style HBackend fill:#E0FFFF
    style Update1 fill:#98FB98
    style Update2 fill:#98FB98
```


# Hotwireを試してみた
では、実際にHotwireを試していきたいと思います。
「星評価機能をサクッと作れるか？」検証です。

![タイトルなし.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/614957/725d06a5-2822-241a-8376-230460a67cdb.gif)



## Turboを使ったフォーム送信
通常、フォーム送信ではページ全体がリロードされますよね。
でも、Turboを有効にするとどうなるか？

```erb
<%= form_with(model: [@review], data: { turbo: true }) do |form| %>
```
これだけで、レビューが投稿されるとフォーム部分だけが動的に更新されます。
読み込みがないので、SPAのようなスムーズな操作感があります！

## Stimulusを使った星評価機能
次に、Stimulusで「どれくらい直感的に実装できるか？」を見ていきます。

### JavaScriptで書いた場合
最初に、Stimulusを使わずJavaScriptで書いた場合です。

```javascript
document.addEventListener('DOMContentLoaded', function() {
  const stars = document.querySelectorAll('.star');
  const ratingInput = document.querySelector('#rating-input');
  
  function initializeStars() {
    stars.forEach((star, index) => {
      star.setAttribute('data-value', index + 1);
      star.classList.add('star-icon');
    });
    updateStars(0);
  }

  function updateStars(rating) {
    stars.forEach((star, index) => {
      if (index < rating) {
        star.classList.add('filled');
      } else {
        star.classList.remove('filled');
      }
    });
  }

  // 1.クリック時の処理
  stars.forEach(star => {
    star.addEventListener('click', (event) => {
      const newRating = parseInt(event.currentTarget.dataset.value);
      ratingInput.value = newRating;
      updateStars(newRating);
      
      const ratingChanged = new CustomEvent('rating-changed', {
        detail: { rating: newRating }
      });
      document.dispatchEvent(ratingChanged);
    });
  });

  // 2.マウスホバー時の処理
  stars.forEach(star => {
    star.addEventListener('mouseover', (event) => {
      const hoverRating = parseInt(event.currentTarget.dataset.value);
      updateStars(hoverRating);
    });

    // 3.ホバー解除時の処理
    star.addEventListener('mouseout', () => {
      const currentRating = parseInt(ratingInput.value) || 0;
      updateStars(currentRating);
    });
  });

  // 初期化
  initializeStars();
});
```
コードが長くなり、管理が大変になりました。

### Stimulusを使った場合
次に、Stimulusを使った場合です。
```javascript
import { Controller } from "@hotwired/stimulus"

export default class extends Controller {
  static targets = ["input", "star"]
  static values = {
    score: { type: Number, default: 0 }
  }

  connect() {
    this.scoreValue = parseInt(this.inputTarget.value) || 0
    this.updateStars()
  }

  setValue(event) {
    const score = parseInt(event.currentTarget.dataset.value)
    this.scoreValue = score
    this.inputTarget.value = score
    this.updateStars()
  }

  // 1.クリック時の処理
  updateStars() {
    this.starTargets.forEach((star, index) => {
      if (index < this.scoreValue) {
        star.classList.add('filled')
        star.classList.remove('empty')
      } else {
        star.classList.remove('filled')
        star.classList.add('empty')
      }
    })
  }

  // 2.マウスホバー時の処理
  preview(event) {
    const score = parseInt(event.currentTarget.dataset.value)
    this.starTargets.forEach((star, index) => {
      if (index < score) {
        star.classList.add('preview')
      } else {
        star.classList.remove('preview')
      }
    })
  }

  // 3.ホバー解除時の処理
  clearPreview() {
    this.starTargets.forEach(star => {
      star.classList.remove('preview')
    })
    this.updateStars()
  }
}
```

星のクリックやホバーの処理をクラスとして管理できました。

「星がクリックされた時に、その値を保存して、クリックされた星までを塗りつぶす」といった流れが直感的に表現できたような気がします。

多少ですがコードも短くなり、メンテナンス性も悪くなさそうです。


# まとめ
Railsでフロントエンド開発を進めるためのHotwireについて解説しました。

最後に学んだことを整理すると、以下のようになります。

- **Turbo**：SPAのように、一部分だけを更新する動作を簡単に実装できる機能
- **Stimulus**：複雑なJavaScriptを書かずに、直感的でメンテナンス性の高いコードを書ける機能
- **Hotwire**：TurboやStimulusなどの機能を統合し、Rails 7.0から導入された便利なツール

Hotwireの「少ない手間でモダンなUIを実現できる」ことの魅力が実感できたのではないでしょうか。

実際に、私は動くところまで試してみて割と簡単にHotwireで実装できたという感覚を持ちました。

ぜひ皆さんも手を動かしながら、Hotwireを使ってみてください！

Railsの新たな可能性や面白さを発見できるはずです。