Rails × Hotwire入門:JavaScriptを書かずにSPAのような高速Webアプリを作る方法
ページ遷移やフォーム送信を高速化したい、複雑なページをコンポーネント単位に分割したい——しかもJavaScriptのコードを一切書かずに。そんな願いを叶えてくれるのがHotwireです。本記事では、サーバーサイドレンダリングを実現するツール「Hotwire」を活用して、Railsアプリケーションを次のレベルへ引き上げる方法を、実例を交えながら詳しく解説します。
Hotwireとは?
Hotwireは、JSONの代わりにHTMLを通信経路上でやり取りすることで、JavaScriptを書かずにモダンなWebアプリケーションを構築できるフレームワークです。HTMLベースの配信により、ページの表示速度は大幅に向上します。
レンダリングはサーバーサイドで維持されるため、Railsが昔から大切にしてきた「シンプルで生産性の高い開発体験」をそのまま享受できます。それでいて、従来のシングルページアプリケーション(SPA)特有の速さや軽快な応答性まで失われないのが魅力です。
Hotwireの中核を担っているのはTurboというgemです。Turboは互いに補完し合う一連の技術群で、以下のような役割を果たします。
- ページ間ナビゲーションの高速化
- フォーム送信の効率化
- 複雑なページのコンポーネント分割
- WebSocket(ActionCable、チャンネル、ストリーミングデータ)を通じたページの部分更新の配信
なぜHotwireを使うべきなのか?
JavaScriptに苦手意識がある方でも、「ページ間の移動がとても速い」という優れたユーザー体験を実現したいなら、Hotwireは最適な選択肢です。
では、Hotwireはどのように動作するのでしょうか。鍵となるのはサーバーサイドレンダリング(SSR)です。SSRはSPAの課題を解決しながら、その主なメリットを維持できるアプローチです。従来型の読み込みのようにレンダリング処理の一部をサーバー側に戻すことで、アプリのロードをより効率的にします。さらに、パフォーマンス改善だけでなく、インデックス登録などのSEO面の問題への対策にも役立ちます。
導入は簡単?
結論から言うと、非常に簡単です。必要なのは以下の3点だけです。
- Railsプロジェクトのデフォルトパッケージ(Ruby、Ruby on Rails、ActionCable、WebSocket)
- JS依存関係をまとめて取得してくれるTurbo gem
- WebSocket接続中の一時データを保存するためのRedis
SPA並みの速度を得るために、わざわざ別の言語(JavaScript)を学ぶ必要はありません。TurboはTurbo Drive、Turbo Frames、Turbo Streams、Turbo Nativeという4つの要素で構成されています。Driveはリンクやフォームを高速化しネットワーク再読み込みを削減し、Framesはページを独立したコンテキストに分割することで、より効率的な読み込みを可能にします。
Railsでの使い方:ステップバイステップ
ここからは、実際にサンプルアプリを構築しながらHotwireの使い方を見ていきましょう。
今回使用する環境・ツールは以下の通りです。
- Ruby
- Ruby on Rails
- Redis
- SQLite(デフォルトデータベース)
- Hotwire gem
- turbo-rails
- StimulusJS
- WebSocket
- ActionCable
作るものは?
今回は練習として、ソーシャルメディア風の投稿アプリを作成します。
まずターミナルを開き、新しいRailsプロジェクトを作成しましょう。
rails new social-media
続いて、プロジェクトのディレクトリに移動します。
cd social-media
Hotwireの追加
Hotwireのgemをプロジェクトに追加します。
bundle add hotwire-rails
あるいは、Gemfileを直接開いて次の行を追記してもOKです。
gem "hotwire-rails"
後者の方法を選んだ場合は、bundleコマンドを実行してください。
bundle install
その後、Hotwire本体をインストールします。
rails hotwire:install
初期設定の確認
ここで、Hotwireが自動で行った初期設定を確認しておきましょう。Gemfileには、次の行が追加されているはずです。
gem 'redis', '~> 4.0'
なぜRedisが必要なのか?
Redisのgemが追加されたのは、ActionCableがWebSocket接続中の一時データを保存するためにRedisを必要とするからです。ただし、Redisをインストールしただけでは使える状態にはなりません。設定が正しいか確認しましょう。config/cable.ymlファイルは、次のようになっているはずです。
development:
adapter: redis
redis://localhost:6379/1
アプリケーション起動時には、Redisサーバー(redis-server)が稼働していることを必ず確認してください。
JS依存関係の確認
package.jsonの依存関係もチェックしておきましょう。
dependencies: {
@hotwired/turbo-rails: ^7.0.0-beta.5,
@rails/actioncable: ^6.0.0,
@rails/activestorage: ^6.0.0,
@rails/ujs: ^6.0.0,
@rails/webpacker: 4.3.0,
stimulus: ^2.0.0
}
モデルの生成
各ファイルの確認が終わったら、postsテーブル用のビュー、コントローラー、モデル、マイグレーションを生成します。カラムはbody(投稿本文)とlikes(いいね数)の2つです。ターミナルで次のコマンドを実行してください。
rails g scaffold posts body:text likes:integer
データベースの作成
すべてのファイルが生成できたら、変更内容をデータベースに反映させます。
rails db:create db:migrate
問題なく完了したら、サーバー(rails server)を起動して動作を確認してみましょう。https://localhost:3000/postsにアクセスすると、投稿ページが表示されるはずです。
投稿一覧画面のスクリーンショット
投稿一覧の表示
次に、投稿を一覧表示できるようにします。app/views/posts/_post.html.erbというファイルを作成し、以下のコードを記述してください。
<div style="background: lightgrey; width: 300px; padding: 10px;">
<%= post.body %>
<br>
<%= link_to :edit, edit_post_path(post) %>
<%= button_to "likes (#{post.likes || 0})", post_path(post, like: true), method: :put %>
</div>
<br>
バリデーションの追加
bodyフィールドが空欄(null)にならないようバリデーションを設定します。あわせて、新規投稿が作成された際に、その投稿を画面の先頭に即座に表示するようブロードキャストの指示も行います。app/models/post.rbを編集し、次のコードを挿入してください。
class Post < ApplicationRecord
validates_presence_of :body
after_create_commit { broadcast_prepend_to :posts }
end
並び順の指定
投稿を新しい順に並べるため、コントローラー(app/controllers/posts_controller.rb)を開いて編集します。
...
def index
@posts = Post.all.order(created_at: :desc)
@post = Post.new
end
...
indexビューの仕上げ
最後に、indexビュー(app/views/posts/index.html.erb)を編集して、「新規投稿フォーム」と「全投稿の一覧」を同じページに表示できるようにします。
<%= turbo_stream_from :posts %>
<%= turbo_frame_tag :post_form do %>
<%= render 'posts/form', post: @post %>
<% end %>
<%= turbo_frame_tag :posts do %>
<%= render @posts %>
<% end %>
indexへのリダイレクト
さらに、コントローラーのcreateアクションを修正します。デフォルトでは投稿後に詳細ページへリダイレクトされますが、すべてを同一ページ上で完結させるため、一覧ページへ戻るように変更しましょう。
...
def create
@post = Post.new(post_params)
respond_to do |format|
if @post.save
format.html { redirect_to posts_path }
format.json { render :show, status: :created, location: @post }
else
format.turbo_stream { render turbo_stream: turbo_stream.replace(@post, partial: 'posts/form', locals: { post: @post }) }
format.html { render :new, status: :unprocessable_entity }
format.json { render json: @post.errors, status: :unprocessable_entity }
end
end
end
...
完成したページは次のような見た目になります。
完成ページのスクリーンショット
ログの確認
投稿を作成すると、ターミナルのログに次のような出力が現れます。
[ActionCable] Broadcasting to posts: "<turbo-stream action=\"prepend\" target=\"posts\"><template><div style=\"background: lightgrey; width: 300px; padding: 10px;\">\n first post\n <br>\n <a href=\"/posts/1/edit\">edit</a>\n <form class=\"button_to\" method=\"post\" action=\"/posts/4?like=true\"><input type=\"hidden\" name=\"_method\" value=\"put\" /><input type=\"submit\" value=\"likes (0)\" /><input type=\"hidden\" name=\"authenticity_token\" value=\"<token>==\" /></form>\n</div>\n<br>\n</template></turbo-stream>"
ここから読み取れること
これは、ActionCableがTurbo Streamsと連携して超高速度で動作していることを意味します。たとえば次のようなレスポンス時間が確認できます。
Completed 302 Found in 18ms
わずか18ミリ秒で処理が完了しています。まさにHotwireの威力と言えるでしょう。
デプロイに関するFAQ
Railsサーバーとは別に、何か実行ファイルが必要?
いいえ、不要です。ActionCableはUnicorn、Puma、Passengerといった主要なサーバーと問題なく連携できます。
HerokuはHotwireに対応している?
はい、対応しています。Hotwireの基盤であるWebSocketはHerokuで利用可能です。さらに詳しく知りたい場合は、Herokuの公式ドキュメントを参照してください。
Redisを使うもうひとつの理由
Redisを採用する理由は、他にもあります。HerokuでRedisを使わずにアプリを複数のdynoへスケールすると、メッセージが全ユーザーに届かなくなるのです。現在のアプリはステートレスな構造のため、1つ目のdynoに接続したクライアントは、2つ目のdynoに接続したクライアントが送信したメッセージを受け取れません。Redisはメッセージの状態をグローバルなストレージに保存することで、この問題を解決します。
HerokuでのRedis設定
リモートのRedisインスタンスとのCLIセッションを確立するには、heroku redis:cliを使用します。インスタンスを指定しない場合は、REDIS_URLに配置されているインスタンスがデフォルトで選択されます。複数のインスタンスを持っている場合は、接続先のインスタンスを明示的に指定してください。
アプリケーションサーバー側ではどう動く?
各dynoがRedisのpub-subシステムを使うように設定します。すべてのdynoは同じチャンネル(デフォルト)を購読し、メッセージを待ち受けます。各サーバーはメッセージを受信すると、それを接続中のクライアントへ配信します。
なお、subscribeはブロッキング関数のため、メッセージ待機中は実行フローが止まります。そのため購読処理は別スレッドで実行します。また、一度ある接続でsubscribeコマンドを発行すると、その接続ではunsubscribeまたはメッセージ受信しかできないため、Redisへの2本目の接続が必要になる点にも注意しましょう。スケーリングの詳細については、関連ドキュメントを参照してください。
セキュリティについて
現状のアプリケーションは、多くの攻撃にさらされる無防備な状態です。本番運用の前には、WSS(WebSocket Secure)の設定と入力値のサニタイズを必ず行ってください。WebSocketセキュリティについては、専用のガイド資料も参考になります。
まとめ
Hotwireは新規アプリケーションで使えるだけでなく、既存のTurbolinksからのアップグレードにも対応しています。
本記事では、「マジックフレームワーク」と呼ばれるHotwireの仕組みを理解し、実際に小さなアプリケーションを構築するところまで体験しました。統合されたTurbo Streamによるリクエスト処理と、WebSocketによる操作・ナビゲーションを組み合わせれば、JavaScriptをほとんど書かずとも、高速でインタラクティブなWebアプリが実現できます。まずは自分のRailsプロジェクトにHotwireを導入して、その開発体験の変化を感じてみてください。
-
RailsでTailwind CSSを使う方法|導入から実践的なスタイリングまで徹底解説
CSSは魔法のような存在ですが、同時に時間のかかる作業でもあります。美しく、機能的で、アクセシブルなサイトは使っていて心地よいものですが、自分でCSSを一から書くのは骨の折れる仕事です。近年はBootstrapをはじめとする多くのCSSフレームワークが登場し、その中でもTailwind CSSは特に注目を集めています。 RailsにはTailwindが標準搭載されていませんが、この記事では新しいRuby on RailsプロジェクトにTailwind CSSを追加する方法を解説します。これにより、デザイン実装にかかる時間を大幅に節約できるでしょう。さらに、Tailwindのユーティリティクラス
-
Rails5でのAngularの使用
あなたは前にその話を聞いたことがあります。分散型で完全に機能するバックエンドAPIと、通常のツールセットで作成されたフロントエンドで実行されているアプリケーションがすでにあります。 次に、Angularに移動します。または、AngularをRailsプロジェクトと統合する方法を探しているだけかもしれません。これは、この方法を好むためです。私たちはあなたを責めません。 このようなアプローチを使用すると、両方の世界を活用して、たとえばRailsとAngularのどちらの機能を使用してフォーマットするかを決定できます。 構築するもの 心配する必要はありません。このチュートリアルは、この目的のた