---
title: "Turboとは？Ruby on RailsでHotwireの画面更新を実装する使い方【Turbo 8.0.23】"
url: "https://www.issoh.co.jp/tech/details/3624/"
published: 2024-09-20
updated: 2026-10-26
categories: ["Ruby on Rails"]
publisher: "株式会社一創"
---

# Turboとは？Ruby on RailsでHotwireの画面更新を実装する使い方【Turbo 8.0.23】

Turboは、JavaScriptをほとんど書かずにページ遷移とフォーム送信をSPAのように速くする、Hotwireの中核フレームワークです。サーバーはJSONでなくHTMLを返し、Turboがそれを受け取って画面の必要な部分だけを差し替えます。Ruby on Railsでは7.0以降の新規アプリに標準で入り、2026年10月26日時点の最新版は `@hotwired/turbo` 8.0.23（2026年1月29日公開）、Rails連携用のgemはturbo-rails 2.0.23です。

この記事では、導入手順、Drive・Frames・Streamsの仕組みと無効化の書き方、一覧をその場で編集・追加する実装例、テスト、Rails UJSからの移行で壊れる箇所までを、Rails 8.1系とturbo-rails 2.0系を前提に扱います。

なお「turbo」の読み方は「ターボ」で、英語ではターボチャージャー（過給機）の略として使われます。検索では同名のファンクラブや自動車部品も多く出ます。この記事で扱うのはWeb開発のTurbo（hotwired/turbo）です。名前の近いVercelのモノレポ用ビルドツールは[Turborepoとは：2.11系のturbo.json設定・キャッシュ共有・CI高速化を実装で解説](/tech/details/17773/)、Next.jsのバンドラは[Turbopackとは：Next.js 16で既定になったRust製バンドラの設定と移行判断](/tech/details/16015/)で扱っており、どちらもTurboとは無関係です。

## まとめ：Ruby on RailsでTurboを使うときの要点

- Turboは Turbo Drive（遷移の高速化）・Turbo Frames（部分差し替え）・Turbo Streams（複数箇所の更新とリアルタイム配信）の3機能からなる。スマホアプリ向けだったTurbo Nativeは、現在Hotwire Nativeとして別リポジトリで開発されている
- 最新は @hotwired/turbo 8.0.23 と turbo-rails 2.0.23（どちらも2026年1月29日公開、2026年10月26日時点）。turbo-rails 2.0.23の動作にはRuby 3.1以上が必要
- Railsでは `rails new` で最初から入る。既存アプリは `bundle add turbo-rails` と `bin/rails turbo:install` の2行で追加できる
- Turbo Driveで状態を変更するフォーム送信に通常のHTMLを返すなら、成功時は303などでリダイレクト、検証エラー時は422などの4xxを返す。リダイレクトなしの200はエラー扱いになる（Frame内の送信とTurbo Streams応答は別の規則）
- 一覧の1行をその場で編集するなら `turbo_frame_tag`、行の追加とフォームのリセットは `create.turbo_stream.erb` で返す
- Turbo 8で、現在のページを取り直す page refresh（設定すればモーフィングで差分だけ反映）と、マウスを乗せたリンクの先読みが入った
- Turbo Driveを止めるなら、要素単位は `data-turbo="false"`、アプリ全体は `Turbo.config.drive.enabled = false`
- 8.0.21以降は、SVGの中に置いたリンクがエラーを出してページ全体を読み直す。修正は未リリースのため、SVGをHTMLの `<a>` で包んで避ける
- Rails UJSからの移行では `data-confirm` と `method: :delete` のリンクが黙って動かなくなる。`data-turbo-confirm` と `data-turbo-method` へ書き換える
- 画面上で複雑な状態を持つ編集ツールやオフライン動作が要るアプリには向かない。サーバーがHTMLを描く前提が崩れるため

## Turboとは：Hotwire・Turbolinksとの関係と2026年10月時点の版

HotwireはBasecamp（現37signals）が公開した「HTMLをサーバーから送る」構成の総称で、Turbo・Stimulus・Hotwire Nativeで構成されます。TurboはそのうちHTMLの取得と差し替えを受け持ち、Stimulusは差し替え後のHTMLに小さな振る舞いを付ける役です。Hotwire全体の構成とStimulusについては[Hotwireとは？Turbo・Stimulus・Hotwire Nativeの役割とRails 8.1での使い方](/tech/details/6745/)にまとめています。

Turbo DriveはTurbolinksの後継です。リンクを横取りする基本は同じで、フォーム送信も横取りするようになり、FramesとStreamsが加わりました。

| 版                  | 公開日        | 主な変化                    |
| ------------------ | ---------- | ----------------------- |
| Turbolinks 5.2.0   | 2018-08-20 | Turbolinksの最終版          |
| Turbo 7.0.0-beta.1 | 2020-12-22 | Turboとして初公開             |
| Turbo 7.0.0        | 2021-09-24 | 正式版。Rails 7の既定に採用       |
| Turbo 8.0.0        | 2024-02-07 | page refresh、先読み、TS廃止   |
| Turbo 8.0.6        | 2024-09-13 | 設定窓口 `Turbo.config` を導入 |
| Turbo 8.0.21       | 2026-01-16 | SVG内リンクの不具合が混入          |
| Turbo 8.0.23       | 2026-01-29 | 2026年10月26日時点の最新        |

日付はnpmレジストリの公開日時で、各版の変更点は[hotwired/turboのリリースノート](https://github.com/hotwired/turbo/releases)で確認できます。8.0.23の公開から9か月近く、npmの `latest` は動いていません。GitHubのmainには2026年6月以降もSVGリンクの修正などがマージされていますが、版としては出ていない状態です。Turbo 8ではソースがTypeScriptからJavaScriptへ戻され（2023年9月6日マージの[Pull Request #971](https://github.com/hotwired/turbo/pull/971)）、8.0.23のnpmパッケージにも型定義は同梱されていません。TypeScriptから使う場合は型を自前で用意するか、型チェックの対象外にします。

Rails側のgemは[rubygems.orgのturbo-rails](https://rubygems.org/gems/turbo-rails)で版を追えます。2.0.23は本体と同じ日に公開され、必要なRubyは3.1以上です。Rails本体は2026年9月24日公開の8.1.4が最新で、以下のコード例はこの組み合わせを前提にしています。

## Railsに導入する：rails newとturbo:installで既存アプリへ追加

Rails 7以降で作ったアプリには、turbo-railsとstimulus-railsが最初から入っています。Rails 8.1.4のジェネレータは、`--skip-hotwire` のほか、Hotwireの省略を伴う `--skip-javascript` や `--api` を付けない限り、両方のgemをGemfileに書いて `turbo:install stimulus:install` を実行します。次のコマンドでTurbo入りのアプリを作り、実装例で使うTaskモデルとテーブルを用意します。

```
rails new todo_app
cd todo_app
bin/rails generate model Task title:string
bin/rails db:migrate
```

モデルやscaffoldを作るジェネレータの使い分けは[Railsジェネレータ（rails generate）の使い方｜モデル・scaffold・カスタム生成まで](/tech/details/2874/)で扱っています。scaffoldが出力するコントローラーは、後述するTurboの応答規則（303と422）に沿ったコードになっています。

既存アプリに追加する場合は、Ruby 3.1以上・Rails 7.1以上で、importmapまたはJavaScriptのビルド環境を設定済みであることが前提です。Rails 7.0以前のアプリは先にRailsを更新します。7.2系からの更新を検討しているなら[Rails 7.2の主な変更点とサポート終了日｜Rails 8.1への移行判断](/tech/details/3362/)も参考になります。

```
bundle add turbo-rails
bin/rails turbo:install
```

`turbo:install` は `config/importmap.rb` があればimportmap用の処理を、なければ `bun.config.js` と `package.json` に応じてBunかYarnでパッケージを追加します。どちらの構成もなければ何も導入しません。importmapで入れる場合は、importmap-railsがGemfileでturbo-railsより上に書かれている必要があります。

JavaScriptの読み込みは `app/javascript/application.js` に置く次の1行で、これで `window.Turbo` も使えるようになります。導入後にブラウザの開発者ツールで `Turbo.session.drive` が `true` を返せば、Turbo Driveが有効です。

```
import "@hotwired/turbo-rails"
```

## Turbo Driveでリンク遷移とフォーム送信を速くする仕組みと設定

Turbo Driveは、同一オリジンへのリンクのクリックとフォーム送信を横取りし、fetchで次のページを取得します。取得したHTMLの `<body>` を差し替え、`<head>` には新しく現れたスタイルシートとスクリプトだけを足すので、読み込み済みのJavaScriptとスタイルを使い回せます。Rails以外で使うなら、有効にするのは `import "@hotwired/turbo"` の1行だけです。

取得に500ミリ秒以上かかると、画面上部にプログレスバーが出ます。この閾値は `Turbo.config.drive.progressBarDelay` で変えられます。PDFやZIPなど拡張子で判定されるファイルへのリンクは、最初から横取りの対象外です。

### フォーム送信の応答は303リダイレクトか422で返すのがTurboの規則

Turbo Drive下のフォームで「送信しても画面が変わらない」ときは、応答のステータスコードを確認します。8.0.23は、Turbo Frameの外で行うGET以外の送信への応答を次のように扱います（Frame内の送信とTurbo Streamsの応答は別の規則）。規則の全体は[Turbo Handbookの「Drive」章](https://turbo.hotwired.dev/handbook/drive)にあります。

| 応答            | Turboの動作          |
| ------------- | ----------------- |
| リダイレクト（303など） | リダイレクト先へ遷移        |
| 200でリダイレクトなし  | エラーを出して何もしない      |
| 4xx（422など）    | 返ってきたHTMLで画面を描き直す |
| 5xx           | エラー画面として描画        |

200で何もしないとき、ブラウザのコンソールには `Form responses must redirect to another location` と出ます。検証エラーでフォームを再表示するなら422を返す必要があり、200のままでは入力エラーが画面に出ません。POST後の200を認めないのは、ブラウザが再読み込み時に出す「フォームを再送信しますか」の確認をTurboでは再現できず、表示中のURLとフォームの送信先もずれるためです。Rails 8.1.4のscaffoldはこの規則どおりのコードを生成します。

```
def create
  @post = Post.new(post_params)

  if @post.save
    redirect_to @post, notice: "Post was successfully created."
  else
    render :new, status: :unprocessable_content
  end
end

def destroy
  @post.destroy!
  redirect_to posts_path, notice: "Post was successfully destroyed.", status: :see_other
end
```

`:unprocessable_content` は422の新しい名前です（Rack 3.1以降）。古いRailsでは `:unprocessable_entity` と書きます。DELETEの後に `status: :see_other`（303）を付けるのは、既定の302だとブラウザがリダイレクト先にもDELETEを送る場合があるためです。

### Turbo 8のリンク先読みを止める3つの方法とサーバー側の注意点

Turbo 8から、マウスカーソルがリンクに乗ると100ミリ秒後に遷移先を先読みします。先読みした結果は10秒間保持され（`<meta name="turbo-prefetch-cache-time">` にミリ秒で指定すれば変更可能）、その間にクリックすれば通信を待たずに表示できます。サーバー側には `X-Sec-Purpose: prefetch` ヘッダー付きのリクエストが届くので、アクセス数の集計や副作用のあるGETには注意が要ります。

先読みだけを止める方法は、範囲に応じて3通りあります。

- サイト全体：`<meta name="turbo-prefetch" content="false">` を `<head>` に置く
- 特定のリンクや領域：`data-turbo-prefetch="false"` を付ける
- JavaScriptから条件付きで止める：`turbo:before-prefetch` イベントで `event.preventDefault()` を呼ぶ

### Turbo Driveを無効にする範囲別の書き方

外部の決済フォームや地図のように、初回の読み込みで初期化される前提のスクリプトがある画面は、そこだけTurbo Driveを外すのが手早い対処です。範囲ごとの書き方は次のとおりです。

| 止める範囲      | 書き方                                   |
| ---------- | ------------------------------------- |
| リンク・フォーム1つ | `data-turbo="false"` を付ける             |
| 領域の中すべて    | 親要素に `data-turbo="false"`             |
| アプリ全体      | `Turbo.config.drive.enabled = false`  |
| そのページへの遷移  | `turbo-visit-control` のmetaで `reload` |

8.0.23は最も近い祖先の `data-turbo` を見るので、止めた領域の中でも `data-turbo="true"` を付けた要素はTurbo Driveに戻ります。アプリ全体で止めたときも、使いたいリンクにだけ `"true"` を付けます。[Turbo Handbookの「Drive」章](https://turbo.hotwired.dev/handbook/drive)に載る `Turbo.session.drive = false` は、同じ設定を書き換える旧来の書き方です。ページ側に `<meta name="turbo-visit-control" content="reload">` を置くと、そのページへの遷移はページ全体の読み込みに切り替わります。フォームまで止めると、422による再描画やTurbo Streamsの応答も使えなくなる点には注意が要ります。

### DOMContentLoadedが遷移後に発火しない問題とturbo:load

Turbo Driveはページを再読み込みしないので、`DOMContentLoaded` は最初の1回しか発火しません。遷移のたびに実行したい処理は `turbo:load` に書きます。Turbolinks時代の `turbolinks:load` も、そのままでは発火しません。

```
document.addEventListener("turbo:load", () => {
  // 遷移のたびに実行される
  initDatePickers()
})
```

要素ごとの初期化が多いなら、Stimulusのコントローラーに移すほうが確実です。Stimulusは要素がDOMに現れた時点で `connect()` を呼ぶため、Turboの遷移やFramesの差し替えを意識せずに済みます。

### SVG内のリンクが8.0.21以降Turboの遷移にならない不具合

8.0.21（2026年1月16日公開）から、`<svg>` の中に置いた `<a href>` をクリックすると、コンソールに `link.href.startsWith is not a function` と出てページ全体の読み込みに落ちます。SVGの `<a>` の `href` は文字列でなく `SVGAnimatedString` を返すためで、原因は[Pull Request #1285](https://github.com/hotwired/turbo/pull/1285)（2025年11月18日マージ）が足した判定だと[Issue #1510](https://github.com/hotwired/turbo/issues/1510)に報告されています。npmパッケージを比べると、リンク判定の関数 `findLinkFromClickTarget` にこの判定が入ったのは8.0.21です。

Playwright同梱のChromium 156で同じHTMLを8.0.20と8.0.23で開き、リンクをクリックした結果が次の表です（「Turbo遷移」は読み直さずに本文が差し替わったこと）。

| クリックした要素            | 8.0.20  | 8.0.23         |
| ------------------- | ------- | -------------- |
| HTMLの `<a>`         | Turbo遷移 | Turbo遷移        |
| SVG内の `<a>`         | Turbo遷移 | 全体読込＋TypeError |
| SVGをHTMLの `<a>` で包む | Turbo遷移 | Turbo遷移        |

リンク先には移動できるので、画面の状態が消えるまで気付きにくい不具合です。修正は2026年6月27日にmainへマージされ、先読みの処理にある同じ判定も直されましたが、2026年10月26日時点で版は出ておらず、turbo-rails 2.0.23が同梱するのも8.0.23です。アイコンやグラフをリンクにするときは、SVGの外側をHTMLの `<a>` で包みます。

## Turbo Frames：同じidのフレームによる一覧行の差し替え

Turbo Framesは、`<turbo-frame>` で囲んだ範囲を独立した小窓として扱う機能です。フレーム内のリンクやフォームの結果は、応答HTMLから同じidのフレームだけを取り出して差し替えます。

```
<turbo-frame id="post_1">
  <h3>記事タイトル</h3>
  <a href="/posts/1/edit">編集</a>
</turbo-frame>

<!-- 表示領域に入ってから読み込む -->
<turbo-frame id="comments" src="/posts/1/comments" loading="lazy">
  読み込み中…
</turbo-frame>
```

応答に同じidのフレームが無いと、フレーム内に「Content missing」と表示され `turbo:frame-missing` イベントが発火します。フレームの外へ遷移させたいリンクに付ける属性は `data-turbo-frame="_top"` です。遅延読み込みや `target` 属性などの詳細は[Turbo Framesとは？turbo\_frame\_tagの使い方とContent missingの対処【Turbo 8.0.23】](/tech/details/6746/)で扱っています。

Railsでは、フレームからのリクエストに `Turbo-Frame` ヘッダーが付くと、turbo-railsがレイアウトを `turbo_rails/frame` に差し替えます。`<head>` にCSRFのmetaタグと `content_for :head` の内容を出力する最小限のレイアウトで、判定用のメソッドは `turbo_frame_request?` です。仕組みの説明は[Turbo Handbookの「Frames」章](https://turbo.hotwired.dev/handbook/frames)にあります。

## Turbo Streams：8アクションによる複数箇所の更新

Turbo Streamsは、1つの応答で画面の複数箇所を更新する仕組みです。サーバーは `text/vnd.turbo-stream.html` という種類のHTMLを返し、その中の `<turbo-stream>` 要素ごとに「どの要素に何をするか」を指定します。8.0.23で使えるアクションは次の8つで、属性の一覧は[Turbo Referenceの「Streams」](https://turbo.hotwired.dev/reference/streams)に載っています。

| action    | 対象要素への操作  |
| --------- | --------- |
| `append`  | 子の末尾に追加   |
| `prepend` | 子の先頭に追加   |
| `replace` | 要素ごと置き換え  |
| `update`  | 中身だけ置き換え  |
| `remove`  | 要素を削除     |
| `before`  | 要素の直前に挿入  |
| `after`   | 要素の直後に挿入  |
| `refresh` | ページ全体を再取得 |

`replace` と `update` に `method="morph"` を付けると、丸ごと置き換える代わりに差分だけを反映します。未保存の入力値まで残る保証はないため、入力欄は一意のidと `data-turbo-permanent` で更新対象から外します。

次のHTMLを `Turbo.renderStreamMessage()` に渡すと、`#messages` への追加、`#message_1` の置き換え、`#flash` の削除が1回で行われます（Playwright同梱のChromium 156と8.0.23で実行して確認）。

```
<turbo-stream action="append" target="messages">
  <template><li id="message_2">新着</li></template>
</turbo-stream>
<turbo-stream action="replace" target="message_1">
  <template><li id="message_1">更新済み</li></template>
</turbo-stream>
<turbo-stream action="remove" target="flash"></turbo-stream>
```

### Railsのフォーム応答で返す方法とAction Cableで配信する方法

届け方は2通りです。1つはフォーム送信への応答で、Turboは GET以外の送信に `text/vnd.turbo-stream.html` を受け付けるヘッダーを付けます。Railsでは `respond_to` で振り分けます。

```
def create
  @message = @room.messages.create!(message_params)

  respond_to do |format|
    format.turbo_stream  # app/views/messages/create.turbo_stream.erb を描画
    format.html { redirect_to @room }
  end
end
```

もう1つはWebSocketやServer-Sent Eventsで、操作した本人以外の画面にも届ける方法です。turbo-railsではビューに `turbo_stream_from` を置き、モデルから `broadcast_append_to` などを呼ぶと、Action Cable経由で購読中のブラウザに配信されます。

```
<%# app/views/rooms/show.html.erb %>
<%= turbo_stream_from @room %>
<div id="messages"><%= render @room.messages %></div>
```

モデル側（`app/models/message.rb`）：

```
class Message < ApplicationRecord
  belongs_to :room
  after_create_commit -> { broadcast_append_to room, target: "messages" }
end
```

開発環境のAction Cableは単一プロセスの `async` アダプタで動くため、`bin/rails console` から配信しても別プロセスのサーバーにつながったブラウザには届きません。[turbo-railsのREADME](https://github.com/hotwired/turbo-rails)が明記している落とし穴です。Action Cable自体の仕組みは[Action Cableとは？Rails 8.1のWebSocket実装とSolid Cable・Redisの選び方](/tech/details/5809/)、配信方式の選び方は[SSEとWebSocketの違いと使い分け｜WebRTC・WebTransportまで性能比較【2026年10月】](/tech/details/4539/)を参照してください。

## Railsの実装例：一覧行の編集・追加画面

ここまでの部品で、行単位の編集と新規追加ができるタスク一覧を、JavaScriptを書かずに作ります。前提は前章の `rails new` と `Task` モデルの作成です。

### タスク画面のルーティングとformat.turbo\_streamによる応答分岐

モデルには空のタイトルを弾く検証を1行足し（`validates :title, presence: true`）、ルーティングは一覧・追加・編集・更新の4つに絞ります。

```
# config/routes.rb
Rails.application.routes.draw do
  resources :tasks, only: %i[index create edit update]
  root "tasks#index"
end
```

コントローラーの要点は `create` の分岐です。保存できたら `create.turbo_stream.erb` を描き、失敗したらフォームだけを描き直すStreamを422付きで返します。Turbo 8.0.23はStreams応答かをContent-Typeだけで判定するので、422のStreamもそのまま反映されます。`format.html` を残せば、JavaScriptが無効な環境でも通常の遷移で動きます。

```
class TasksController < ApplicationController
  before_action :set_task, only: %i[edit update]

  def index
    @tasks = Task.order(:id)
    @task = Task.new
  end

  def create
    @task = Task.new(task_params)

    respond_to do |format|
      if @task.save
        format.turbo_stream
        format.html { redirect_to tasks_path, status: :see_other }
      else
        format.turbo_stream do
          render turbo_stream: turbo_stream.update("new_task", partial: "tasks/form", locals: { task: @task }),
                 status: :unprocessable_content
        end
        format.html do
          @tasks = Task.order(:id)
          render :index, status: :unprocessable_content
        end
      end
    end
  end

  def edit
  end

  def update
    if @task.update(task_params)
      redirect_to tasks_path, status: :see_other
    else
      render :edit, status: :unprocessable_content
    end
  end

  private

  def set_task
    @task = Task.find(params[:id])
  end

  def task_params
    params.expect(task: [:title])
  end
end
```

`params.expect` はRails 8.0で入った書き方で、7.x系では `params.require(:task).permit(:title)` と書きます。`update` は一覧へ303でリダイレクトしますが、フレーム内の送信なので、Turboはリダイレクト先から同じidのフレーム（編集した行）だけを取り出して差し替えます。

### turbo\_frame\_tag：一覧行と編集フォームのid統一

行の部分テンプレートと編集画面を、同じ `turbo_frame_tag task` で囲むのが要です。レコードを渡すと `dom_id` が使われ、idは `task_1` のようになります。

```
<%# app/views/tasks/index.html.erb %>
<h1>タスク</h1>
<div id="tasks">
  <%= render @tasks %>
</div>
<div id="new_task">
  <%= render "form", task: @task %>
</div>

<%# app/views/tasks/_task.html.erb %>
<%= turbo_frame_tag task do %>
  <%= task.title %>
  <%= link_to "編集", edit_task_path(task) %>
<% end %>

<%# app/views/tasks/edit.html.erb %>
<%= turbo_frame_tag @task do %>
  <%= render "form", task: @task %>
  <%= link_to "取消", tasks_path %>
<% end %>

<%# app/views/tasks/_form.html.erb %>
<%= form_with model: task do |f| %>
  <% task.errors.full_messages.each do |message| %>
    <p><%= message %></p>
  <% end %>
  <%= f.text_field :title %>
  <%= f.submit %>
<% end %>
```

「取消」もフレーム内のリンクなので、一覧から同じidの行だけを戻します。ほかの行は描き直されず、スクロール位置もそのままです。

### create.turbo\_stream.erb：行追加と入力欄リセットの応答

追加に成功したときに返すStreamは2つです。1つ目で一覧の末尾に行を足し、2つ目で入力欄を空の新規フォームに戻します。`turbo_stream.append` にレコードを渡すと、`tasks/_task` の部分テンプレートが自動で選ばれます。

```
<%# app/views/tasks/create.turbo_stream.erb %>
<%= turbo_stream.append "tasks", @task %>
<%= turbo_stream.update "new_task", partial: "tasks/form", locals: { task: Task.new } %>
```

開発者ツールのネットワーク欄では、追加の応答が `text/vnd.turbo-stream.html`、編集リンクのリクエストに `Turbo-Frame: task_1` ヘッダーが付くことで動作を確かめられます。他の利用者の画面にも追加を反映するなら、Taskモデルに `after_create_commit -> { broadcast_append_to :tasks, target: "tasks" }` を、一覧ビューに `<%= turbo_stream_from :tasks %>` を追加します。

## Turbo 8：ページリフレッシュとモーフィングによる差分反映

Turbo 8.0.0で入った page refresh は、現在のページを再取得して更新する機能です。既定ではbodyを置き換え、`turbo-refresh-method` に `morph` を指定すると差分反映になります。差分反映にはidiomorphを使い、設定の詳細は[Turbo Handbookの「Page Refreshes」章](https://turbo.hotwired.dev/handbook/page%5Frefreshes)にあります。

```
<%# レイアウトの head 内 %>
<%= turbo_refreshes_with method: :morph, scroll: :preserve %>

<%# 出力されるmetaタグ %>
<meta name="turbo-refresh-method" content="morph">
<meta name="turbo-refresh-scroll" content="preserve">
```

モデル側に `broadcasts_refreshes` を書くと、変更のたびに `refresh` アクションが配信されます。turbo-rails 2.0.23のソースでは、作成時はモデル名の複数形（例：`"messages"`）のストリームへ、更新・削除時はそのレコード自身のストリームへ配信します。新規作成を一覧画面に反映させたいなら、一覧側で `turbo_stream_from :messages` のように複数形の名前でも購読しておくことが必要です。

page refreshは更新箇所を考えずに済む代わりに、サーバーが毎回ページ全体を描画します。更新頻度が高い画面や重い一覧では、Streamsで対象を絞るほうが軽く済みます。

## Turboを使う画面をRailsのテストで確かめる3つのヘルパー

turbo-rails 2.0.23は、読み込まれた時点で `assert_turbo_stream`（返したStreamのactionとtarget）、`assert_turbo_frame`（指定idのフレームの有無）、`assert_turbo_stream_broadcasts`（モデルからの配信の有無と回数）をRailsのテストクラスへ自動で組み込みます（`lib/turbo/engine.rb` の初期化処理）。次は前章の `create` を結合テストで確かめる例です。`as: :turbo_stream` を付けると、Turboと同じAcceptヘッダーでリクエストが送られます。ブロックを渡せば、Streamの中身も `assert_select` で検査できます。

```
# test/controllers/tasks_controller_test.rb
require "test_helper"

class TasksControllerTest < ActionDispatch::IntegrationTest
  test "追加するとtasksの末尾に行を足すStreamを返す" do
    post tasks_path, params: { task: { title: "牛乳を買う" } }, as: :turbo_stream

    assert_response :success
    assert_turbo_stream action: "append", target: "tasks" do
      assert_select "template turbo-frame", text: /牛乳を買う/
    end
  end

  test "空のタイトルは422とフォームの描き直しを返す" do
    post tasks_path, params: { task: { title: "" } }, as: :turbo_stream

    assert_response :unprocessable_content
    assert_turbo_stream status: :unprocessable_content, action: "update", target: "new_task"
  end
end
```

RSpecでも[request specとは？Rails 8・RSpec 8でのリクエストスペックの書き方](/tech/details/3993/)の形で同じヘルパーを呼べます。結合テストで分かるのは「どのStreamを返したか」までで、画面で行が差し替わることはシステムテストで確かめます。Capybaraから実ブラウザを動かす構成は[RSpecでCapybaraとPlaywrightを使う設定手順｜capybara-playwright-driverとRailsの注意点](/tech/details/6355/)で扱っています。

## Rails UJSからTurboへ移行すると黙って壊れる3か所と書き換え

Rails 7より前のアプリをTurboに移すと、エラーも出ずに動作だけが変わる箇所があります。Rails UJSが担っていた属性をTurboは解釈しないためです。`data: { confirm: "..." }` は `data: { turbo_confirm: "..." }` に、`link_to ..., method: :delete` は `data: { turbo_method: :delete }` か `button_to` に、`remote: true` は外してHTMLかStreamsの応答に書き換えます。最も気付きにくいのは確認ダイアログです。Rails UJSを外した状態で `data-confirm` を残すと、Turboは確認ダイアログを出しません。削除フォームや `data-turbo-method="delete"` のリンクでは、確認なしで削除要求が送られます。`data-turbo-confirm` はフォーム送信時にブラウザ標準の `confirm()` を呼び、独自のダイアログに替えたい場合は `Turbo.config.forms.confirm` に関数を渡します。

```
<%= link_to "削除", post_path(@post),
      data: { turbo_method: :delete, turbo_confirm: "削除しますか？" } %>

<%# フォームを生成するのでJavaScriptが無効でも動く %>
<%= button_to "削除", post_path(@post), method: :delete,
      form: { data: { turbo_confirm: "削除しますか？" } } %>
```

`data-turbo-method` 付きのリンクは、Turboが内部でフォームを組み立てて送信します。JavaScriptが動かない環境ではただのGETリンクになるので、削除のように取り消せない操作は `button_to` にしておくのが安全です。Rails UJSと併用する場合の段階的な手順は、turbo-railsのUPGRADING.mdにあります。

`remote: true` で `.js.erb` を返していた画面は、機械的な置き換えでは済みません。返していたJavaScriptが「どの要素をどう変えるか」を読み取り、同じ変更をTurbo Streamsのアクションで表し直します。`$("#list").append(...)` なら `append`、`$("#item").remove()` なら `remove` という対応で、前章の実装例がそのまま書き換え先の型になります。

## Rails以外のサーバーでTurboを使うときの前提とhtmxとの違い

Turboの本体 `@hotwired/turbo` はRailsに依存しないnpmパッケージで、MITライセンスです。サーバー側の条件は、HTMLを返すことと、フォームの応答で前述のステータスコード規則を守ることだけです。Streamsを返すときはContent-Typeを `text/vnd.turbo-stream.html` にします。

turbo-railsが提供する `turbo_frame_tag` や `broadcasts_refreshes` のようなヘルパーは、ほかのフレームワークでは各コミュニティのパッケージ（SymfonyのSymfony UX Turboなど）に頼るか、自前で書きます。同じ「HTMLを返す」発想でHTML属性だけで部分更新するhtmxとの違いは[htmxの使い方：hx-属性の実装手順からFastAPI連携まで解説【2026年版】](/tech/details/15371/)で比較できます。Turboは既定で同一オリジンなどの条件を満たすリンクとフォームを横取りし、htmxは属性を付けた要素だけを対象にする、という既定の動作の差が選定の分かれ目です。

## Turboを採用すべきでない場面と業務システムで採用する判断基準

Turboは「画面の状態はサーバーが持ち、ブラウザは受け取ったHTMLを表示する」前提で成り立っています。この前提が崩れるアプリでは、Stimulusを積み増すほど構成が複雑になり、ReactやVueで組んだほうが素直です。

- 図形エディタや表計算のように、操作のたびにサーバーへ問い合わせられない細かな状態を画面が持つ
- オフラインでの新規データ操作や画面遷移を主機能にする（Turbo単体ではオフライン用データ保存や同期処理を提供しない）
- 同じAPIをスマホアプリや外部サービスにもJSONで提供する必要があり、HTMLとJSONの二重実装になる

逆に、管理画面・予約フォーム・社内の業務システムのように「一覧→詳細→編集→保存」を繰り返すアプリでは、APIとフロントエンドを分けずに済むTurboの利点が最も出ます。実装例のとおり行の編集や追加はコントローラーとビューの数ファイルで完結し、フロントエンド専任者を置かずにRailsの開発者だけで画面を作り切れます。Bootstrapは見た目を整えるCSSフレームワークで、Turboとは担当する層が違うため、両方を同じアプリで併用できます。

判断に迷ったら、画面の操作を「サーバーに保存してから表示を変えるもの」と「保存せずに画面だけで完結するもの」に分けて数えてみてください。前者が大半ならTurboで足り、後者が画面の中心を占めるならSPAの構成を検討します。Railsで管理画面や業務システムを新たに作る、あるいは既存のRails UJSのアプリをTurboへ移したいといった相談は、[業務用・Webアプリ開発](/service/apps/business/)で要件整理から実装、移行後の保守まで受けています。

## よくある質問

### 「turbo」の意味と読み方は？

読み方は「ターボ」です。英単語としてはターボチャージャー（過給機）の略として使われます。Web開発でTurboと言う場合は、37signalsが開発するHotwireのフレームワーク hotwired/turbo を指すのが一般的です。

### TurboとHotwireの違いは？

Hotwireは構成全体の名前で、TurboはそのうちHTMLの取得と差し替えを担う部品です。Hotwireには他にStimulus（JavaScriptの振る舞い）とHotwire Native（iOS・Androidアプリ）が含まれます。Ruby on Railsでは turbo-rails と stimulus-rails の2つのgemとして入ります。

### Turbo Nativeは今も使える？

Turbo Nativeの後継はHotwire Nativeです。Turbo NativeとStradaを1つにまとめたもので、iOS版・Android版とも1.0.0が2024年9月25日に公開されました。旧turbo-iosのREADMEは既存アプリにもHotwire Nativeへの早期移行を求めています。2026年10月26日時点の最新版はiOSが1.3.1（2026年8月14日）、Androidが1.3.2（2026年10月12日）です。WebのTurbo DriveとFramesで作った画面を、そのままネイティブアプリの画面として表示できます。

### TurboとTurborepo・Turbopackは関係がある？

関係はありません。TurborepoとTurbopackはVercelが開発するJavaScriptのビルド関連ツールで、TurboはHotwireのフロントエンドフレームワークです。Turborepoのコマンド名が `turbo` であるため、検索結果で混ざりやすくなっています。

### Turbo DriveのJavaScriptが遷移後に動かないのはなぜ？

`DOMContentLoaded` は初回の文書読み込み時にしか発火しないためです（body内のscript要素はTurboの描画時にも評価されます）。`turbo:load` に書き換えるか、Stimulusのコントローラーへ移します。画面単位で外したいなら `data-turbo="false"` を使います。

## 関連記事

- [Hotwireとは？Turbo・Stimulus・Hotwire Nativeの役割とRails 8.1での使い方](/tech/details/6745/)
- [Turbo Framesとは？turbo\_frame\_tagの使い方とContent missingの対処【Turbo 8.0.23】](/tech/details/6746/)
- [Action Cableとは？Rails 8.1のWebSocket実装とSolid Cable・Redisの選び方](/tech/details/5809/)
- [SSEとWebSocketの違いと使い分け｜WebRTC・WebTransportまで性能比較【2026年10月】](/tech/details/4539/)
- [htmxの使い方：hx-属性の実装手順からFastAPI連携まで解説【2026年版】](/tech/details/15371/)

---

出典: [Turboとは？Ruby on RailsでHotwireの画面更新を実装する使い方【Turbo 8.0.23】](<https://www.issoh.co.jp/tech/details/3624/>)（株式会社一創）
