Rails Turbo and Hotwire in 2026: Real-Time Applications and Interview Questions
Master Rails Turbo and Hotwire for building real-time web applications. Deep dive into Turbo 8 morphing, Turbo Streams broadcasting, Stimulus integration, and interview preparation.

Rails Turbo and Hotwire deliver real-time interactivity to web applications without requiring custom JavaScript frameworks. The 2026 ecosystem has matured significantly, with Turbo 8's morphing capabilities and Rails 8's enhanced broadcasting making reactive UIs achievable with minimal code. This deep dive covers production patterns, interview preparation, and implementation details for building modern Rails applications.
Turbo 8's morphing with broadcasts_refreshes enables real-time updates with a single line of Ruby code. The server broadcasts a signal, clients morph the DOM, and scroll position is preserved automatically.
Understanding the Hotwire Architecture in 2026
Hotwire consists of three complementary technologies that work together to create reactive web experiences. Turbo Drive intercepts navigation and form submissions, replacing page content via AJAX without full reloads. Turbo Frames scope updates to specific page regions, enabling inline editing and lazy loading. Turbo Streams deliver granular DOM changes over WebSocket connections or HTTP responses.
The architecture follows a server-centric model where HTML remains the primary payload. Unlike SPAs that transfer JSON and render on the client, Hotwire applications render HTML server-side and stream it to browsers. This approach simplifies state management, improves SEO, and reduces JavaScript bundle sizes.
Rails 8 ships with Hotwire 2.0 by default, integrating Action Cable for WebSocket support. The framework handles connection management, channel subscriptions, and broadcasting without manual configuration.
Turbo Drive: Zero-Configuration SPA Navigation
Turbo Drive transforms standard links and forms into AJAX-powered interactions. When enabled, clicking any link triggers a fetch request, the response body replaces the current page content, and the browser history updates appropriately.
# app/views/layouts/application.html.erb
<!DOCTYPE html>
<html>
<head>
<%= turbo_refreshes_with method: :morph, scroll: :preserve %>
<%= stylesheet_link_tag "application", "data-turbo-track": "reload" %>
<%= javascript_importmap_tags %>
</head>
<body>
<%= yield %>
</body>
</html>The data-turbo-track: "reload" attribute ensures asset changes trigger full page reloads, preventing stale CSS or JavaScript. The turbo_refreshes_with helper configures morphing behavior globally, though it can be overridden per-page.
Disabling Turbo Drive for specific links requires the data-turbo attribute:
<!-- Disable Turbo for external links -->
<%= link_to "Download PDF", document_path(@doc, format: :pdf), data: { turbo: false } %>
<!-- Force full page reload -->
<%= link_to "Admin Panel", admin_path, data: { turbo_action: "replace" } %>Turbo Frames: Scoped Page Updates
Turbo Frames isolate page sections into independently updatable regions. A frame wraps content with a unique identifier, and any navigation within that frame updates only the wrapped content.
<!-- app/views/comments/index.html.erb -->
<%= turbo_frame_tag "comments" do %>
<% @comments.each do |comment| %>
<%= render comment %>
<% end %>
<%= link_to "Load more", comments_path(page: @page + 1) %>
<% end %>The server response must include a matching frame tag. Turbo extracts the frame content and replaces the existing frame in the DOM:
<!-- app/views/comments/index.html.erb (paginated response) -->
<%= turbo_frame_tag "comments" do %>
<% @comments.each do |comment| %>
<%= render comment %>
<% end %>
<% if @comments.any? %>
<%= link_to "Load more", comments_path(page: @page + 1) %>
<% end %>
<% end %>Lazy loading frames defer content loading until the frame enters the viewport:
<%= turbo_frame_tag "user_stats", src: user_stats_path(@user), loading: :lazy do %>
<div class="animate-pulse bg-gray-200 h-24"></div>
<% end %>Turbo Streams: Real-Time DOM Manipulation
Turbo Streams provide seven actions for manipulating the DOM: append, prepend, replace, update, remove, before, and after. Each action targets an element by ID and applies the specified change.
<!-- app/views/comments/create.turbo_stream.erb -->
<%= turbo_stream.prepend "comments" do %>
<%= render @comment %>
<% end %>
<%= turbo_stream.update "comment_count" do %>
<%= @comments_count %> comments
<% end %>
<%= turbo_stream.replace "new_comment_form" do %>
<%= render "form", comment: Comment.new %>
<% end %>Controllers respond to Turbo Stream requests alongside HTML:
# app/controllers/comments_controller.rb
class CommentsController < ApplicationController
def create
@comment = @post.comments.build(comment_params)
respond_to do |format|
if @comment.save
format.turbo_stream
format.html { redirect_to @post, notice: "Comment added" }
else
format.turbo_stream do
render turbo_stream: turbo_stream.replace(
"new_comment_form",
partial: "form",
locals: { comment: @comment }
)
end
format.html { render :new, status: :unprocessable_entity }
end
end
end
endReady to ace your Ruby on Rails interviews?
Practice with our interactive simulators, flashcards, and technical tests.
Turbo 8 Morphing: Intelligent DOM Updates
Turbo 8 introduced morphing, powered by the idiomorph library. Instead of replacing entire page sections, morphing compares the current DOM with incoming HTML and applies minimal changes. This preserves form state, scroll position, and focus.
# app/models/message.rb
class Message < ApplicationRecord
belongs_to :conversation
broadcasts_refreshes
endThe broadcasts_refreshes callback schedules a background job when records change. The job broadcasts a refresh signal to all subscribed clients via Action Cable. Clients fetch the current page HTML and morph their DOM to match.
<!-- app/views/conversations/show.html.erb -->
<%= turbo_stream_from @conversation %>
<div id="messages">
<%= render @conversation.messages %>
</div>
<%= render "messages/form", conversation: @conversation %>The turbo_stream_from helper subscribes the page to a Turbo Streams channel. When any message in the conversation changes, all viewers see updates instantly.
Morphing Configuration Options
Fine-grained control over morphing behavior uses data attributes:
<!-- Preserve element across morphs -->
<div data-turbo-permanent id="video-player">
<video src="<%= @video.url %>" autoplay></video>
</div>
<!-- Skip morphing, always replace -->
<div data-turbo-method="replace" id="chart-container">
<%= render "chart", data: @analytics %>
</div>The refresh debouncer prevents excessive broadcasts. When multiple database updates occur rapidly, only one refresh triggers after a 500ms delay. This is configurable per model:
# app/models/task.rb
class Task < ApplicationRecord
broadcasts_refreshes debounce: 1.second
endInterview Questions: Hotwire Fundamentals
Technical interviews frequently assess Hotwire knowledge through conceptual and implementation questions.
Q: How does Turbo Drive differ from traditional AJAX navigation libraries?
Turbo Drive intercepts all navigation automatically without explicit configuration. It preserves the browser's back/forward button behavior, updates the URL via pushState, and handles redirects transparently. The key distinction is progressive enhancement: applications work without JavaScript, gaining speed improvements when JavaScript loads.
Q: When should Turbo Frames be preferred over Turbo Streams?
Turbo Frames suit navigation-based interactions where clicking a link should update a specific page region. Frames maintain URL semantics and work with browser history. Turbo Streams handle broadcast scenarios where multiple elements need updating simultaneously or where updates originate from background processes rather than user navigation.
Q: Explain the morphing refresh cycle in Turbo 8.
When broadcasts_refreshes triggers, Rails enqueues Turbo::Streams::BroadcastStreamJob. The job broadcasts a refresh action to the stream name derived from the model. Subscribed clients receive the action, fetch the current page via GET request, and pass both DOMs to idiomorph. The library calculates the minimal diff and applies changes, preserving elements marked data-turbo-permanent.
Building a Real-Time Notification System
A practical implementation demonstrates Hotwire's capabilities for real-time features:
# app/models/notification.rb
class Notification < ApplicationRecord
belongs_to :user
after_create_commit :broadcast_to_user
private
def broadcast_to_user
broadcast_prepend_to(
"notifications_#{user_id}",
target: "notifications",
partial: "notifications/notification",
locals: { notification: self }
)
broadcast_update_to(
"notifications_#{user_id}",
target: "unread_count",
html: user.notifications.unread.count.to_s
)
end
endThe view subscribes to the user-specific channel:
<!-- app/views/layouts/_navbar.html.erb -->
<nav>
<%= turbo_stream_from "notifications_#{current_user.id}" %>
<div class="relative">
<button>Notifications</button>
<span id="unread_count" class="badge">
<%= current_user.notifications.unread.count %>
</span>
<div id="notifications" class="dropdown">
<%= render current_user.notifications.recent %>
</div>
</div>
</nav>When a notification is created anywhere in the system, the user's navbar updates instantly without polling.
Performance Optimization Patterns
Production applications benefit from several optimization techniques documented in the Rails 8 production guide.
Eager Loading Stream Subscriptions
Avoid N+1 subscription patterns in lists:
# app/controllers/projects_controller.rb
def index
@projects = current_user.projects.includes(:tasks)
end<!-- Subscribe once per project, not per task -->
<% @projects.each do |project| %>
<%= turbo_stream_from project %>
<%= render project %>
<% end %>Selective Broadcasting
Limit broadcasts to relevant changes:
# app/models/post.rb
class Post < ApplicationRecord
broadcasts_refreshes_to ->(post) { post.published? ? :published_posts : :draft_posts }
endCaching with Morphing
Fragment caching works alongside morphing when cache keys incorporate timestamps:
<% cache [@conversation, @conversation.messages.maximum(:updated_at)] do %>
<%= render @conversation.messages %>
<% end %>Common Pitfalls and Solutions
Several issues frequently arise when implementing Hotwire applications.
JavaScript Initialization: Stimulus controllers must account for morphing. Use connect() and disconnect() callbacks properly, and avoid storing state that morphing would invalidate.
import { Controller } from "@hotwired/stimulus"
export default class extends Controller {
connect() {
this.chart = new Chart(this.element, this.chartConfig)
}
disconnect() {
this.chart?.destroy()
}
get chartConfig() {
return JSON.parse(this.element.dataset.chartConfig)
}
}Form State Preservation: Morphing preserves form inputs by default. For forms that should reset after submission, clear them explicitly in the Turbo Stream response:
<%= turbo_stream.replace "message_form" do %>
<%= render "form", message: Message.new %>
<% end %>Scroll Position Jumps: The scroll: :preserve option helps, but complex layouts may need explicit scroll anchoring:
<div data-turbo-scroll="preserve" id="message_list">
<%= render @messages %>
</div>Stimulus Integration for Enhanced Interactivity
While Turbo handles most interactions, Stimulus fills gaps requiring client-side logic. The combination keeps JavaScript focused and minimal.
import { Controller } from "@hotwired/stimulus"
export default class extends Controller {
static targets = ["form"]
submit() {
clearTimeout(this.timeout)
this.timeout = setTimeout(() => {
this.formTarget.requestSubmit()
}, 300)
}
}<div data-controller="auto-submit">
<%= form_with model: @search, data: { auto_submit_target: "form" } do |f| %>
<%= f.text_field :query,
data: { action: "input->auto-submit#submit" },
placeholder: "Search..." %>
<% end %>
</div>For more advanced patterns, review the caching strategies and Rails testing approaches modules.
Conclusion
- Turbo Drive provides SPA-like navigation without configuration by intercepting links and forms
- Turbo Frames scope updates to page regions, enabling inline editing and lazy loading
- Turbo Streams broadcast DOM changes via seven actions: append, prepend, replace, update, remove, before, after
- Turbo 8 morphing with
broadcasts_refreshesdelivers real-time updates using idiomorph for minimal DOM changes - Preserve elements across morphs with
data-turbo-permanent - Use
turbo_stream_fromin views paired with model callbacks for broadcasting - The 500ms default debounce prevents excessive broadcasts during rapid updates
- Stimulus controllers should use proper
connect/disconnectlifecycle methods for morphing compatibility - Fragment caching remains effective when cache keys incorporate timestamps
Start practicing!
Test your knowledge with our interview simulators and technical tests.
Tags
Share
Related articles

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.

Action Cable and WebSockets in Rails: Complete Guide for Technical Interviews 2026
Deep dive into Action Cable and WebSockets in Ruby on Rails. Covers connections, channels, broadcasting, Solid Cable in Rails 8, scaling with Redis, and common interview questions with code examples.

Rails API Mode in 2026: RESTful APIs, Serialization and Best Practices
Master Rails API mode with best practices for RESTful design, JSON serialization with Alba and jsonapi-serializer, authentication strategies, and error handling in Rails 8.