--- name: hotwire-turbo description: Best practices for using Hotwire Turbo to create reactive applications --- # 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/](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 `` content while keeping the `` 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 ```erb <%= 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 ``, 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:** ```erb <%# In your layout or page head %> <%# Optionally preserve scroll position %> ``` **Per-element control:** ```erb <%# Keep an element from being morphed (preserves exact state) %>
``` **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 `` tag. When a link or form inside the frame is activated, only that frame's content is replaced. **Best practices:** ```erb <%= turbo_frame_tag @invoice do %>
<%= @invoice.number %> <%= link_to "Edit", edit_invoice_path(@invoice) %>
<% end %> <%= turbo_frame_tag @invoice do %> <%= form_with model: @invoice do |f| %> <% 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"`: ```erb <%= turbo_frame_tag "comments", src: comments_path, loading: "lazy" do %>

Loading comments...

<% 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 `` elements that specify actions (`append`, `prepend`, `replace`, `update`, `remove`, `before`, `after`, `morph`, `refresh`) and target DOM elements by ID. **Best practices:** ```ruby # 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 ``` ```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:** ```erb <%# 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)