# 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. - Published: 2026-07-19 - Updated: 2026-07-19 - Author: SharpSkill - Tags: rails, turbo, hotwire, real-time, websockets, stimulus - Reading time: 12 min --- 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. > **Key Takeaway** > > 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](https://turbo.hotwired.dev/) 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](/technologies/ruby-on-rails/interview-questions/action-cable-websockets) 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. ```ruby # app/views/layouts/application.html.erb <%= turbo_refreshes_with method: :morph, scroll: :preserve %> <%= stylesheet_link_tag "application", "data-turbo-track": "reload" %> <%= javascript_importmap_tags %> <%= yield %> ``` 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: ```erb <%= link_to "Download PDF", document_path(@doc, format: :pdf), data: { turbo: false } %> <%= 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. ```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: ```erb <%= 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: ```erb <%= turbo_frame_tag "user_stats", src: user_stats_path(@user), loading: :lazy do %>
<% 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. ```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: ```ruby # 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 end ``` ## Turbo 8 Morphing: Intelligent DOM Updates Turbo 8 introduced [morphing](https://turbo.hotwired.dev/handbook/page_refreshes), 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. ```ruby # app/models/message.rb class Message < ApplicationRecord belongs_to :conversation broadcasts_refreshes end ``` The `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. ```erb <%= turbo_stream_from @conversation %>
<%= render @conversation.messages %>
<%= 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: ```erb
<%= render "chart", data: @analytics %>
``` 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: ```ruby # app/models/task.rb class Task < ApplicationRecord broadcasts_refreshes debounce: 1.second end ``` ## Interview 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: ```ruby # 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 end ``` The view subscribes to the user-specific channel: ```erb ``` 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](https://rubylearning.com/blog/2026/04/12/rails-8-production-performance-security-hotwire/). ### Eager Loading Stream Subscriptions Avoid N+1 subscription patterns in lists: ```ruby # app/controllers/projects_controller.rb def index @projects = current_user.projects.includes(:tasks) end ``` ```erb <% @projects.each do |project| %> <%= turbo_stream_from project %> <%= render project %> <% end %> ``` ### Selective Broadcasting Limit broadcasts to relevant changes: ```ruby # app/models/post.rb class Post < ApplicationRecord broadcasts_refreshes_to ->(post) { post.published? ? :published_posts : :draft_posts } end ``` ### Caching with Morphing Fragment caching works alongside morphing when cache keys incorporate timestamps: ```erb <% 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. ```javascript // app/javascript/controllers/chart_controller.js 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: ```erb <%= 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: ```erb
<%= render @messages %>
``` ## Stimulus Integration for Enhanced Interactivity While Turbo handles most interactions, [Stimulus](https://stimulus.hotwired.dev/) fills gaps requiring client-side logic. The combination keeps JavaScript focused and minimal. ```javascript // app/javascript/controllers/auto_submit_controller.js import { Controller } from "@hotwired/stimulus" export default class extends Controller { static targets = ["form"] submit() { clearTimeout(this.timeout) this.timeout = setTimeout(() => { this.formTarget.requestSubmit() }, 300) } } ``` ```erb
<%= form_with model: @search, data: { auto_submit_target: "form" } do |f| %> <%= f.text_field :query, data: { action: "input->auto-submit#submit" }, placeholder: "Search..." %> <% end %>
``` For more advanced patterns, review the [caching strategies](/technologies/ruby-on-rails/interview-questions/caching-strategies) and [Rails testing approaches](/technologies/ruby-on-rails/interview-questions/rails-testing-rspec) 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_refreshes` delivers real-time updates using idiomorph for minimal DOM changes - Preserve elements across morphs with `data-turbo-permanent` - Use `turbo_stream_from` in views paired with model callbacks for broadcasting - The 500ms default debounce prevents excessive broadcasts during rapid updates - Stimulus controllers should use proper `connect`/`disconnect` lifecycle methods for morphing compatibility - Fragment caching remains effective when cache keys incorporate timestamps --- 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/rails-turbo-hotwire-2026-real-time-applications