header image

枝折

Hotwire の Turbo と Stimulus を触ってみて

Ruby
JavaScript

CREATED: 2026 / 05 / 05 Tue

UPDATED: 2026 / 05 / 05 Tue

これまで使ってこなかった Turbo と Stimulus にようやく触れてみる。

Hotwire とは

Hotwire は Rails 7 からデフォルトで組み込まれた、JavaScript をほとんど書かずにリッチな UI を実現するためのフレームワークです。 3つの技術で構成されています。

Turbo Drive   → ページ遷移を高速化(自動、コード不要)
Turbo Frame   → ページの一部を GET リクエストで更新
Turbo Stream  → フォーム送信後に複数箇所を DOM 操作

それぞれ役割がはっきりと分かれており、操作の種類に応じて使い分けます。

Turbo Drive

仕組み

Turbo Drive はページ遷移を高速化する仕組みで、特別なコードを書かなくても Turbo を導入するだけで自動的に有効になります。

通常のブラウザはリンクをクリックするとページ全体をリクエストし、HTML・CSS・JS をすべて読み込み直します。 Turbo Drive はこれを次のように変えます。

# 通常のブラウザ(Drive なし)
リンククリック
  → ページ全体をリクエスト
  → HTML, CSS, JS を全部読み込み直し
  → ページ全体を再描画

# Turbo Drive あり
リンククリック
  → ページ全体をリクエスト
  → <body> だけ差し替え
  → <head> の CSS, JS は再読み込みしない  ← ここが速い

CSS や JS が増えれば増えるほど Drive の恩恵が大きくなります。

Drive はクライアントサイドの JavaScript ライブラリです。初回レンダリングは通常のブラウザと変わりませんが、2回目以降のページ遷移から効果を発揮します。JavaScript が無効な場合は通常のページ遷移にフォールバックします。

Prefetch

Turbo Drive にはリンクにマウスをホバーしただけで裏側でページを先読みする Prefetch 機能があります。

リンクにホバー → 裏側で fetch 開始
クリック       → すでに取得済みなので即座に表示

ユーザーがリンクをクリックするまでの数百ミリ秒の間にフェッチを済ませておくことで、体感速度を大幅に向上できます。

data-turbo-track: “reload”

CSS が変更されたときに Drive が自動でページをリロードする仕組みです。

<link rel="stylesheet" href="/assets/application.css" data-turbo-track="reload">

Drive は通常 <body> だけを差し替えますが、data-turbo-track="reload" を指定した要素の href が変わっていた場合はページ全体をリロードします。 デプロイ後に古い CSS が適用され続けてしまう問題を防ぐための仕組みです。

通常のページ遷移(Drive)
  → <head> を比較
  → data-turbo-track の href が変わっていたら → ページ全体をリロード
  → 変わっていなければ → <body> だけ差し替え(通常の Drive の動作)

Turbo Frame

基本的な使い方

Turbo Frame はページの一部だけを更新する仕組みです。リンククリック(GET リクエスト)に対応しています。

turbo_frame_tag ヘルパーでフレームを定義します。

<%# ページ上のフレーム %>
<%= turbo_frame_tag "new_todo" do %>
  <%= render "form", todo: @todo %>
<% end %>

以下の HTML が生成されます。

<turbo-frame id="new_todo">
  ...
</turbo-frame>

Edit リンクをクリックすると次の流れで動作します。

Edit リンクをクリック
  → GET /todos/1/edit(HTTP リクエスト発生)
  → edit.html.erb を返す
  → レスポンスから id が一致する turbo-frame だけ抽出して差し替え
  → 他のフレームは一切触れない

サーバー側は普通のページを返すだけで、差し替えはブラウザ側の Turbo が担当します。

dom_id で一意な ID を生成

複数のレコードを一覧表示する場合、dom_id ヘルパーでレコードごとに一意な ID を生成します。

dom_id(todo)  # todo.id が 1 なら → "todo_1"
              # todo.id が 2 なら → "todo_2"
<%= turbo_frame_tag dom_id(todo) do %>
<turbo-frame id="todo_1">...</turbo-frame>
<turbo-frame id="todo_2">...</turbo-frame>

todo ごとに独立したフレームになるため、複数の todo が並んでいても正しい行だけを更新できます。

テンプレートはパーシャルではなく通常の erb

edit.html.erb はパーシャル(_edit.html.erb)ではなく通常のビューファイルです。 Turbo Frame はブラウザから GET /todos/1/edit という HTTP リクエストを送り、そのレスポンスから対応する turbo-frame を抽出します。 パーシャルにしてしまうと URL でアクセスできなくなるため、通常のビューとして定義する必要があります。

Turbo Stream

Turbo Frame との違い

Turbo FrameTurbo Stream
リクエストGET(リンククリック)POST/PATCH/DELETE(フォーム送信)
更新箇所1フレームのみ複数箇所同時に可能

Turbo Frame はリンクのクリック、Turbo Stream はフォームの送信後に使うのが基本的な使い分けです。

7つのアクション

Turbo Stream には DOM を操作する7つのメソッドがあります。

メソッド動作
prepend要素の先頭に追加
append要素の末尾に追加
before要素のに挿入
after要素のに挿入
replace要素を丸ごと置き換え
update要素の中身だけ置き換え
remove要素を削除

使用例

create.turbo_stream.erb では複数の DOM 操作を1レスポンスでまとめて記述できます。

<%# create.turbo_stream.erb %>
<%= turbo_stream.prepend "todos-list", partial: "todos/todo", locals: { todo: @todo } %>
<%= turbo_stream.replace "new_todo" do %>
  <%= turbo_frame_tag "new_todo" do %>
    <%= render "form", todo: Todo.new %>
  <% end %>
<% end %>

1つ目は新しい todo をリストの先頭に追加し、2つ目はフォームを空の状態にリセットしています。 このように1回のレスポンスで複数箇所を同時に更新できるのが Turbo Stream の強みです。

respond_to での使い方

コントローラーでは respond_to ブロックを使って Turbo Stream レスポンスと通常の HTML レスポンスを出し分けます。

respond_to do |format|
  format.turbo_stream        # Accept: text/vnd.turbo-stream.html のとき選ばれる
  format.html { redirect_to todos_path }  # Accept: text/html のとき選ばれる
end

Turbo が送るリクエストには Accept: text/vnd.turbo-stream.html ヘッダーが自動的に付きます。 これにより format.turbo_stream が選ばれ、対応するテンプレートファイル(create.turbo_stream.erb など)が自動探索されます。

# ブロックなし → create.turbo_stream.erb を自動探索
format.turbo_stream

# ブロックあり → インラインで Turbo Stream 命令を組み立てる
format.turbo_stream do
  render turbo_stream: turbo_stream.replace("new_todo") { ... }
end

Stimulus

Stimulus は HTML に data-* 属性を書くことで JavaScript の動作を紐付けるフレームワークです。 Turbo と組み合わせることで、サーバー側のレンダリングを維持しながらインタラクティブな UI を実現できます。

3つの基本概念

data-controller → コントローラーのスコープを定義
data-action     → イベントとメソッドを紐付ける
data-target     → DOM 要素を参照する
data-value      → データを渡す

Controllers

data-controller が付いた要素とその子要素の中だけでコントローラーが動作します。

<span data-controller="checkbox">   ← スコープ開始
  <input data-checkbox-target="input" />  ← スコープ内
</span>                              ← スコープ終了

<span class="todo-title">...</span>  ← スコープ外、関係ない

同じコントローラーが複数あっても、それぞれ独立したインスタンスとして動作します。

Targets

data-[controller名]-target で DOM 要素を参照します。

static targets = ['input']
data-checkbox-target="input"
this.inputTarget  // → <input> 要素そのものを参照

Values

data-[controller名]-[value名]-value でデータを渡します。

static values = { url: String }
data-checkbox-url-value="/todos/1"
this.urlValue  // → "/todos/1"

Actions

data-action でイベントとメソッドを紐付けます。

data-action="[イベント名]->[コントローラー名]#[メソッド名]"
data-action="change->checkbox#toggle"
change    → チェックボックスの状態が変わったとき
checkbox  → checkbox_controller.js
toggle    → toggle() メソッドを呼ぶ

よく使うイベントは次の通りです。

イベント発生タイミング
clickクリックしたとき
change値が変わったとき
submitフォームを送信したとき
keyupキーを離したとき
mouseoverマウスが乗ったとき

要素の種類によってデフォルトのイベントが決まっているので省略できます。

data-action="change->checkbox#toggle"
data-action="checkbox#toggle"          ← 同じ意味(checkbox のデフォルトは change)

コントローラーの自動読み込み

*_controller.js という命名規則を守るだけで設定変更不要で自動的に読み込まれます。

app/javascript/controllers/checkbox_controller.js
  → data-controller="checkbox" として自動登録

読み込みの流れは次の通りです。

application.html.erb
  └─ javascript_importmap_tags
       └─ application.js
            └─ import "controllers"(index.js)
                 └─ eagerLoadControllersFrom("controllers", application)
                      └─ checkbox_controller.js を自動検出・登録

使い分けまとめ

CRUD 操作ごとにどの技術を使うかを整理するとこうなります。

操作使用技術理由
Edit リンククリックTurbo FrameGET リクエストのため
Cancel リンククリックTurbo FrameGET リクエストのため
Create 成功Turbo Streamリスト追加+フォームリセットの2箇所を同時更新
Update 成功Turbo Streamフォーム送信のレスポンスのため
DeleteTurbo Streamフォーム送信のレスポンスのため
チェックボックス切り替えStimulus + Turbo Streamボタンなしで即時反映するため

Turbo Frame は GET、Turbo Stream は POST/PATCH/DELETE と覚えておくと判断しやすいです。 Stimulus はボタンを押さずにイベントを拾いたい場合や、JavaScript で動的な振る舞いを加えたい場合に出番があります。

を仕舞い

参考資料📕