# 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
```
### 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
<%= 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
<% 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