# Ruby on Rails 7: Hotwire and Turbo for Reactive Applications > Complete guide to Hotwire and Turbo in Rails 7. Learn to build reactive applications without writing JavaScript using Turbo Drive, Frames, and Streams. - Published: 2026-01-11 - Updated: 2026-03-28 - Author: SharpSkill - Tags: ruby on rails, hotwire, turbo, turbo frames, turbo streams - Reading time: 12 min --- Rails 7 revolutionized web development by integrating Hotwire by default. This stack enables building highly reactive applications without writing a single line of custom JavaScript. Turbo Drive, Turbo Frames, and Turbo Streams replace traditional SPA approaches with an "HTML over the wire" philosophy. > **Why Hotwire?** > > Hotwire drastically reduces frontend complexity. No need for React or Vue for dynamic interfaces: the server sends ready-to-use HTML, and Turbo handles DOM updates automatically. ## Understanding the Hotwire Architecture Hotwire consists of three complementary technologies that work together to deliver a smooth user experience without the typical JavaScript framework complexity. **Turbo Drive** accelerates navigation by intercepting link clicks and form submissions. Instead of reloading the entire page, only the `` content gets replaced, preserving JavaScript and CSS context. **Turbo Frames** decompose pages into independent sections. Each frame can be updated separately, enabling targeted interactions without affecting the rest of the page. **Turbo Streams** enable real-time updates via WebSocket or in response to HTTP requests. Eight CRUD actions are available for declarative DOM manipulation. ```ruby # Gemfile # Installing Turbo Rails (included by default in Rails 7+) gem 'turbo-rails' ``` For existing projects, installation requires just one command. ```bash # installation.sh # Installing Turbo in an existing Rails project rails turbo:install # Verify the JavaScript import is present cat app/javascript/application.js # Should contain: import "@hotwired/turbo-rails" ``` ## Initial Turbo Drive Configuration Turbo Drive is enabled by default in Rails 7. All navigation automatically becomes "turbo" without additional configuration. Behavior can be customized via data attributes. ```erb <%= csrf_meta_tags %> <%= csp_meta_tag %> <%= stylesheet_link_tag "application", "data-turbo-track": "reload" %> <%= javascript_importmap_tags %>
<%= yield %> ``` Turbo Drive can be disabled on specific links or forms when necessary. ```erb <%= link_to "Dashboard", dashboard_path %> <%= link_to "Download PDF", export_path, data: { turbo: false } %> <%= form_with model: @document, data: { turbo: false } do |f| %> <%= f.file_field :attachment %> <%= f.submit "Upload" %> <% end %> <%= link_to "Delete", item_path(@item), data: { turbo_method: :delete, turbo_confirm: "Are you sure?" } %> ``` > **Turbo Drive Cache** > > Turbo Drive caches visited pages. The `data-turbo-track="reload"` attribute on assets forces a full reload if CSS/JS files change. ## Turbo Frames for Targeted Updates Turbo Frames define page zones that update independently. Each frame has a unique identifier and only responds to responses containing a matching frame. ```erb

Messages

<%= render @messages %>
<%= link_to "Load more", messages_path(page: @next_page) %>
<%= render "form", message: Message.new %> ``` The server response must contain a frame with the same identifier for the update to work. ```erb

<%= message.content %>

<%= link_to "Edit", edit_message_path(message) %> <%= button_to "Delete", message_path(message), method: :delete %>
``` ### Lazy Loading with Turbo Frames Frames can load their content asynchronously using the `src` attribute. Initial content displays during loading. ```erb

Dashboard

Loading statistics...

Loading notifications...

<%= render "activity/placeholder" %> ``` The controller responds with a view containing only the requested frame. ```ruby # app/controllers/dashboard_controller.rb class DashboardController < ApplicationController def stats @user_count = User.count @message_count = Message.count @active_today = User.where("last_seen_at > ?", 24.hours.ago).count # Partial rendering for the frame render partial: "dashboard/stats" end end ``` ```erb

<%= @user_count %>

Users

<%= @message_count %>

Messages

<%= @active_today %>

Active today

``` ## Turbo Streams for Real-Time Updates Turbo Streams offer eight actions for DOM manipulation: `append`, `prepend`, `replace`, `update`, `remove`, `before`, `after`, and `morph`. These actions can be triggered via HTTP response or WebSocket. ```ruby # app/controllers/messages_controller.rb class MessagesController < ApplicationController def create @message = current_user.messages.build(message_params) respond_to do |format| if @message.save # Turbo Stream response to add the message format.turbo_stream do render turbo_stream: [ turbo_stream.prepend("messages", @message), turbo_stream.update("message_count", partial: "messages/count"), turbo_stream.replace("new_message", partial: "messages/form", locals: { message: Message.new }) ] end format.html { redirect_to messages_path, notice: "Message created" } else format.turbo_stream do render turbo_stream: turbo_stream.replace( "new_message", partial: "messages/form", locals: { message: @message } ) end format.html { render :new, status: :unprocessable_entity } end end end def destroy @message = Message.find(params[:id]) @message.destroy respond_to do |format| # Removal with animation format.turbo_stream { render turbo_stream: turbo_stream.remove(@message) } format.html { redirect_to messages_path, notice: "Message deleted" } end end end ``` ### Dedicated Turbo Stream Templates For more complex responses, a `.turbo_stream.erb` template offers more flexibility. ```erb <%= turbo_stream.prepend "messages" do %> <%= render @message %> <% end %> <%= turbo_stream.update "message_count" do %> <%= Message.count %> messages <% end %> <%= turbo_stream.replace "new_message" do %> <%= render "form", message: Message.new %> <% end %> <%= turbo_stream.prepend "flash_messages" do %>
Message sent successfully!
<% end %> ``` > **Response Format** > > Turbo Stream responses must have the content-type `text/vnd.turbo-stream.html`. Rails handles this automatically with `respond_to` and the `turbo_stream` format. ## Real-Time Broadcasting with Action Cable Turbo Streams truly shine with WebSocket broadcasting. Users receive updates instantly without polling. ```ruby # app/models/message.rb class Message < ApplicationRecord belongs_to :room belongs_to :author, class_name: "User" # Automatic broadcast after creation after_create_commit do broadcast_append_to( room, target: "messages", partial: "messages/message", locals: { message: self } ) end # Broadcast after update after_update_commit do broadcast_replace_to( room, target: dom_id(self), partial: "messages/message", locals: { message: self } ) end # Broadcast after deletion after_destroy_commit do broadcast_remove_to(room, target: dom_id(self)) end end ``` The view subscribes to the corresponding stream with the `turbo_stream_from` helper. ```erb

<%= @room.name %>

<%= turbo_stream_from @room %>
<%= render @room.messages.order(created_at: :asc) %>
<%= form_with model: [@room, Message.new], id: "new_message" do |f| %> <%= f.text_area :content, placeholder: "Your message..." %> <%= f.submit "Send" %> <% end %> ``` ### Asynchronous Broadcasting for Heavy Operations To avoid blocking requests, broadcasting can be performed in the background. ```ruby # app/models/report.rb class Report < ApplicationRecord belongs_to :user after_create_commit :generate_async private def generate_async GenerateReportJob.perform_later(self) end end ``` ```ruby # app/jobs/generate_report_job.rb class GenerateReportJob < ApplicationJob queue_as :default def perform(report) # Simulating a long operation report.update!(status: "processing") # Notify user of start report.broadcast_replace_to( report.user, :reports, target: dom_id(report), partial: "reports/report" ) # Generate the report result = ReportGenerator.new(report).generate report.update!(content: result, status: "completed") # Notify user of completion report.broadcast_replace_to( report.user, :reports, target: dom_id(report), partial: "reports/report" ) end end ``` ## Advanced Patterns with Turbo ### Inline Form Editing A common pattern involves replacing static content with an inline edit form. ```erb
<%= task.content %>
<%= link_to "Edit", edit_task_path(task) %> <%= button_to "Delete", task_path(task), method: :delete, data: { turbo_confirm: "Delete this task?" } %>
``` ```erb <%= form_with model: @task do |f| %> <%= f.text_field :content, autofocus: true %>
<%= f.submit "Save" %> <%= link_to "Cancel", task_path(@task) %>
<% end %>
``` ### Navigation Outside the Parent Frame By default, links in a frame stay within that frame. The `data-turbo-frame` attribute allows targeting another frame or the entire page. ```erb ``` ```erb
<%= yield %>
``` > **Debugging Turbo** > > In development, open the JavaScript console and type `Turbo.setProgressBarDelay(0)` to see the progress bar immediately. `Turbo.session.drive = false` temporarily disables Turbo Drive. ## Error Handling and Loading States Good UX requires handling loading states and network errors. ```erb <%= form_with model: @model, data: { controller: "form-loading", action: "turbo:submit-start->form-loading#disable turbo:submit-end->form-loading#enable" } do |f| %> <%= yield f %> <% end %> ``` ```javascript // app/javascript/controllers/form_loading_controller.js import { Controller } from "@hotwired/stimulus" export default class extends Controller { static targets = ["submit", "text", "spinner"] disable() { this.submitTarget.disabled = true this.textTarget.classList.add("hidden") this.spinnerTarget.classList.remove("hidden") } enable() { this.submitTarget.disabled = false this.textTarget.classList.remove("hidden") this.spinnerTarget.classList.add("hidden") } } ``` ### Server Error Handling ```ruby # app/controllers/application_controller.rb class ApplicationController < ActionController::Base rescue_from ActiveRecord::RecordNotFound do |exception| respond_to do |format| format.turbo_stream do render turbo_stream: turbo_stream.replace( "main_content", partial: "shared/not_found" ) end format.html { render "errors/not_found", status: :not_found } end end end ``` ## Performance Optimization Several techniques ensure performant Turbo applications. ```ruby # app/controllers/messages_controller.rb class MessagesController < ApplicationController # Avoid N+1 queries with eager loading def index @messages = Message.includes(:author, :room) .order(created_at: :desc) .page(params[:page]) end # Fragment caching for static elements def show @message = Message.find(params[:id]) fresh_when @message end end ``` ```erb <% cache message do %>
<%= message.content %> By <%= message.author.name %>
<% end %> ``` ## Conclusion Hotwire and Turbo transform Rails development by eliminating the complexity of traditional JavaScript frameworks. Turbo Drive accelerates navigation, Turbo Frames enable targeted updates, and Turbo Streams offer powerful real-time capabilities. ### Checklist for Getting Started with Hotwire - ✅ Use Rails 7+ for built-in Hotwire integration - ✅ Understand the three components: Drive, Frames, and Streams - ✅ Identify page zones that benefit from Turbo Frames - ✅ Use `respond_to` with `format.turbo_stream` in controllers - ✅ Implement broadcasting for real-time features - ✅ Combine with Stimulus for simple JavaScript interactions This "HTML over the wire" approach enables building modern, reactive applications while maintaining the simplicity and productivity that make Ruby on Rails so powerful. The result: less code to maintain, excellent performance, and an optimal developer experience. --- Source: SharpSkill (https://sharpskill.dev), tech interview preparation for your real stack. HTML version of this page: https://sharpskill.dev/en/blog/ruby-on-rails/ruby-on-rails-7-hotwire-turbo