Back to skills
extension
Category: Development & EngineeringNo API key required

hotwire-turbo

Best practices for using Hotwire Turbo to create reactive applications

personAuthor: jakexiaohubgithub

Hotwire Best Practices for Reactive Applications

Rule updated on 12/15/2025 to Turbo version 8.0.18.

Hotwire consists of three main components, each suited for different use cases. Here's when to use each.

For full reference see https://turbo.hotwired.dev/

Turbo Drive (Default Page Navigation)

When to use:

  • Standard page-to-page navigation (it's on by default)
  • Full page updates where you want faster transitions without a full browser reload
  • Simple CRUD operations where you're redirecting after an action

How it works: Intercepts link clicks and form submissions, fetches the new page via AJAX, and swaps the <body> content while keeping the <head> intact.

Best practices:

  • It's automatic—you get it for free in Rails 8
  • Use data-turbo="false" to disable for specific links/forms (e.g., file downloads, external links)
  • Use data-turbo-method="delete" for non-GET requests from links
<%= link_to "Delete", invoice_path(@invoice), data: { turbo_method: :delete, turbo_confirm: "Are you sure?" } %>

Turbo 8 Morphing (Smooth Page Refreshes)

When to use:

  • Form submissions where you want to preserve scroll position
  • Updates that should feel seamless without visible "flash"
  • Pages with user input that shouldn't be lost during refresh
  • Maintaining focus state and CSS transitions during updates

How it works: Instead of replacing the entire <body>, morphing intelligently diffs the current DOM against the new HTML and applies only the necessary changes. This preserves:

  • Scroll position
  • Form input values
  • Focus state
  • Active CSS transitions/animations

Enabling morphing:

<%# In your layout or page head %>
<meta name="turbo-refresh-method" content="morph">

<%# Optionally preserve scroll position %>
<meta name="turbo-refresh-scroll" content="preserve">

Per-element control:

<%# Keep an element from being morphed (preserves exact state) %>
<div data-turbo-permanent id="chat-messages">
  <!-- Content preserved across page updates -->
</div>

When NOT to use morphing:

  • When you want a clear visual transition between pages
  • When the page structure changes dramatically
  • When you need to reset all page state

Turbo Frames (Partial Page Updates)

When to use:

  • Inline editing (edit-in-place forms)
  • Lazy loading content sections
  • Modal dialogs or slideovers
  • Tabbed interfaces
  • Pagination within a section
  • Any time you want to update a specific region without touching the rest

How it works: Wraps a section of the page in a <turbo-frame> tag. When a link or form inside the frame is activated, only that frame's content is replaced.

Best practices:

<!-- index.html.erb -->
<%= turbo_frame_tag @invoice do %>
  <div class="invoice-row">
    <%= @invoice.number %>
    <%= link_to "Edit", edit_invoice_path(@invoice) %>
  </div>
<% end %>

<!-- edit.html.erb -->
<%= turbo_frame_tag @invoice do %>
  <%= form_with model: @invoice do |f| %>
    <!-- form fields -->
  <% end %>
<% end %>
  • Use turbo_frame_tag with a model — Rails generates consistent IDs (invoice_123)
  • Break out of frames with data-turbo-frame="_top" for full-page navigation
  • Lazy load with src and loading: "lazy":
    <%= turbo_frame_tag "comments", src: comments_path, loading: "lazy" do %>
      <p>Loading comments...</p>
    <% end %>
    
  • Target other frames with data-turbo-frame="frame_id"

Turbo Streams (Real-Time DOM Manipulation)

When to use:

  • Updating multiple parts of the page from a single action
  • Real-time updates via WebSockets (ActionCable)
  • Adding/removing items from lists without full refresh
  • Flash messages after form submissions
  • Counter/badge updates
  • Any scenario where you need surgical DOM updates

How it works: Returns <turbo-stream> elements that specify actions (append, prepend, replace, update, remove, before, after, morph, refresh) and target DOM elements by ID.

Best practices:

# Controller
def create
  @invoice = current_user.invoices.build(invoice_params)

  respond_to do |format|
    if @invoice.save
      format.turbo_stream  # Renders create.turbo_stream.erb
      format.html { redirect_to @invoice }
    else
      format.html { render :new, status: :unprocessable_entity }
    end
  end
end
<!-- create.turbo_stream.erb -->
<%= turbo_stream.prepend "invoices", @invoice %>
<%= turbo_stream.update "invoice_count", Invoice.count %>
<%= turbo_stream.remove "empty_state" %>

Stream Actions:

| Action | Description | | --------- | --------------------------------------------------- | | append | Add to end of target container | | prepend | Add to beginning of target container | | replace | Replace the entire target element | | update | Replace only the content (innerHTML) | | remove | Remove the target element | | before | Insert before the target | | after | Insert after the target | | morph | Morph the target element (intelligent diff) | | refresh | Trigger a full page refresh (optionally with morph) |

Morph stream action example:

<%# Smoothly update a section without replacing it entirely %>
<%= turbo_stream.morph "invoice_#{@invoice.id}", partial: "invoices/invoice", locals: { invoice: @invoice } %>

Decision Matrix

| Scenario | Solution | | --------------------------- | --------------------------------- | | Standard navigation | Turbo Drive (automatic) | | Edit form inline | Turbo Frame | | Load section lazily | Turbo Frame with src | | Add item to list | Turbo Stream append/prepend | | Update multiple areas | Turbo Stream | | Real-time via WebSocket | Turbo Stream over ActionCable | | Delete from list | Turbo Stream remove | | Modal/slideout | Turbo Frame targeting a container | | Preserve scroll on refresh | Morphing with turbo-refresh | | Smooth inline update | Turbo Stream morph | | Update without losing focus | Morphing |


Key Principles

  1. Progressive Enhancement — Start with Turbo Drive, add Frames for scoped updates, then Streams for complex interactions
  2. Minimize JavaScript — Use Stimulus only when you need client-side behavior that can't be achieved with Turbo
  3. Semantic IDs — Use model-based IDs (dom_id(@invoice)) for reliable targeting
  4. Graceful Degradation — Always have an html format fallback for non-Turbo requests
  5. Keep Frames Small — Smaller frames = faster updates and easier maintenance
  6. Use Morphing for Polish — Enable morphing when smooth transitions matter (forms, live updates)