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 を実現するための仕組みで、以下の要素で成り立っています。

Turbo Drive   → ページ遷移を高速化(自動、コード不要)
Turbo Frame   → ページの一部を GET リクエストで更新
Turbo Stream  → POST/PUT/PATCH/DELETE 時に複数箇所を更新
Stimulus      → Turbo を利用しながら JavaScript で画面に動きをつける

と言ってもこれら自体は JavaScript でできてます🙆‍♂️

Turbo Drive

仕組み

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

これまでの Rails アプリケーションではページ全体をリクエストし、HTML・CSS・JS をすべて読み込み直していましたが、Turbo Drive を使うと以下のようになります。

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

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

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

初回レンダリングは通常のブラウザと変わりませんが、2回目以降のページ遷移から効果を発揮します。

ちなみに、ブラウザの JavaScript が無効な場合は通常のページ遷移にフォールバックします。

Turbo Frame

基本的な使い方

Turbo Frame はページの一部だけを更新する仕組みです。

GET リクエスト(Rails コントローラーの show や edit)で取得した HTML を対応する turbo-frame タグのコンポーネントと置き換えることができます。

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

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

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

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

この HTML を返却する API を呼び出すと、そのレスポンスに含まれる id と一致する turbo-frame だけを抽出して、クライアントが部分更新を行います(他のフレームには影響しない)。

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

dom_id で一意な ID を生成

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

dom_id(item)  # item.id が 1 なら → "item_1"
              # item.id が 2 なら → "item_2"
<%= turbo_frame_tag dom_id(item) do %>
<turbo-frame id="item_1">...</turbo-frame> <turbo-frame id="item_2">...</turbo-frame>

これによって id の照合ができるというわけです。

Turbo Stream

Turbo Frame との違い

Turbo FrameTurbo Stream
リクエストGETPOST/PUT/PATCH/DELETE
更新箇所1フレームのみ複数箇所同時に可能

Turbo Frame は GET リクエストによって取得した HTML を部分的に置き換えのために、Turbo Stream は POST/PUT/PATCH/DELETE リクエストの完了時に発生する画面の更新(主に複数の箇所の更新)のために利用することができます。

7つのアクション

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

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

使用例

create.turbo_stream.erb には複数の画面更新処理をまとめることができます。

<%# create.turbo_stream.erb %>
<%= turbo_stream.prepend "items-list", partial: "items/item", locals: { item: @item } %>
<%= turbo_stream.replace "new_item" do %>
  <%= turbo_frame_tag "new_item" do %>
    <%= render "form", item: Item.new %>
  <% end %>
<% end %>

turbo_stream.prepend は新しい item をリストの先頭に追加し、turbo_stream.replace はフォームを空の状態にリセットしています。 このように、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 items_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_item") { ... }
end

ブラウザで JavaScript が無効になっている場合は Accept ヘッダーが自動でつきません。 そういう場合は、format.html が選ばれ、従来通りの方法でクライアントにページが返されます。

Stimulus

Stimulus を使うことで、Turbo を利用しながら JavaScript(コントローラーに定義されます) をいい感じに利用することができます。 Turbo と組み合わせてインタラクティブな UI を実現できます。

3つの基本概念

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

Controllers

data-controller が付いた要素とその子要素の中だけで JavaScript が動作します。

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

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

Targets

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

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

Values

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

static values = { url: String }
data-[controller名]-url-value="/items/1"
this.urlValue // → "/items/1"

Actions

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

data-action="[イベント名]->[controller名]#[メソッド名]"
data-action="change->checkbox#toggle"

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

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

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

*_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 を自動検出・登録

を仕舞い

参考資料📕