Hotwire — Turbo & Stimulus

Ruby on Rails Intermediate/Advanced — Hotwire: Turbo & Stimulus
Ruby on Rails Intermediate/Advanced
Course 2 · Chapter 7 · Hotwire: Turbo & Stimulus

🔥 Hotwire: Turbo & Stimulus

Chapter 6 reached for raw WebSockets when a feature genuinely needed the server to speak first. But most "this page feels sluggish" complaints don't need that — they need faster navigation and small sprinkles of interactivity on top of pages Rails already renders. Hotwire ("HTML Over The Wire") is Rails' answer: keep rendering ERB on the server, and use a small amount of JavaScript to make that HTML feel like a modern app, without adopting a full frontend framework's component model.

🚗 Turbo Drive — Faster Navigation, No Extra Code

Turbo Drive is on by default in a new Rails app and requires no code at all: it intercepts every link click and form submission, fetches the new page in the background, and swaps in just the <body> — instead of the browser doing a full page reload (fresh HTML, fresh CSS/JS parse, a blank flash in between). The URL bar and history still update normally; the user experience gets SPA-like speed while every page is still a plain server-rendered response, no client-side router required.

🖼️ Turbo Frames — Independent Page Regions

<%= turbo_frame_tag "post_#{@post.id}" do %> <h3><%= @post.title %></h3> <%= link_to "Edit", edit_post_path(@post) %> <% end %>

A turbo_frame_tag marks off a region of the page that can be updated independently. Clicking the "Edit" link inside it still requests the full edit page normally, but Turbo notices the response contains a matching turbo_frame_tag with the same ID, and swaps in just that fragment — the rest of the page (navigation, sidebar, other posts) never re-renders. The controller action itself needs no special code; the edit view just needs to wrap its content in a frame with the same id.

🌊 Turbo Streams — Targeted Updates from Anywhere

# app/controllers/comments_controller.rb def create @comment = Comment.new(comment_params) @comment.save! respond_to do |format| format.turbo_stream # renders create.turbo_stream.erb automatically end end
# app/views/comments/create.turbo_stream.erb <%= turbo_stream.append "comments", partial: "comments/comment", locals: { comment: @comment } %>

A Turbo Stream response is a small chunk of HTML tagged with an action (append, prepend, replace, remove) and a target element ID. This one appends a newly rendered comment partial (the same partial from rails2-2's conditional-links challenge) onto an element with id comments — no full page reload, no hand-written JavaScript DOM manipulation of the kind js1-8 covered.

💎 Turbo Streams Reuse Chapter 6's Action Cable Under the Hood

Rails 7's broadcasts_to model helper (broadcasts_to ->(comment) { comment.post }, inserts_by: :append) generates Turbo Stream broadcasts automatically from a model callback — the exact same after_create_commit-triggered broadcast pattern Chapter 6 wrote by hand for CommentsChannel, except the payload is a rendered HTML fragment instead of JSON, and Turbo's client-side JavaScript applies it to the DOM without any received callback needing to be written.

🎮 Stimulus — JavaScript Behavior on Existing HTML

// app/javascript/controllers/clipboard_controller.js import { Controller } from "@hotwired/stimulus" export default class extends Controller { static targets = ["source"] copy() { navigator.clipboard.writeText(this.sourceTarget.value) } }
<div data-controller="clipboard"> <input data-clipboard-target="source" value="share-link-here" readonly> <button data-action="click->clipboard#copy">Copy</button> </div>

A Stimulus controller attaches behavior to HTML that already exists (server-rendered by ERB) rather than generating that HTML itself — data-controller connects the JS class to the element, data-action wires an event to a method, data-*-target gives the controller a handle on a specific child element. There's no virtual DOM, no component re-rendering, no client-side state to keep in sync with the server — the HTML is the source of truth, and Stimulus just adds small, scoped behavior on top of it.

📜 Hotwire vs a Full Frontend Framework

Hotwire (Rails) vs React/Vue/Svelte (already covered)

ConcernReact/Vue/SvelteHotwire
RenderingClient-side, from a JS component treeServer-side ERB, always
StateExplicit client state, synced to the serverServer is the only source of truth
Data fetchingClient calls a JSON API, re-rendersServer pushes/returns HTML fragments directly
JS footprintThe entire UI is JavaScriptSmall, scoped behavior only (Stimulus)
Best fitRich, highly interactive client appsContent-driven apps wanting SPA-like feel cheaply

Neither approach is strictly better — a heavily interactive dashboard with lots of client-only state genuinely benefits from React's component model; a mostly content-driven app (a blog, a forum, an admin panel) often gets 90% of the "feels fast and modern" experience from Hotwire for a fraction of the JavaScript, while keeping every rendering decision in one place: the Rails server.

💻 Coding Challenges

Challenge 1: A Turbo Frame

Wrap a post's title and an "Edit" link in a turbo_frame_tag keyed to that post's id, so clicking "Edit" replaces just that region with the edit form instead of navigating to a full new page.

Goal: Practice scoping a page update to one independent region.

→ Solution

Challenge 2: A Turbo Stream Response

Write a destroy action on a CommentsController that responds with a turbo_stream format, and the corresponding destroy.turbo_stream.erb view that removes the deleted comment's element from the page.

Goal: Practice a non-append Turbo Stream action (remove) triggered from a real controller action.

→ Solution

Challenge 3: A Stimulus Controller

Write a Stimulus controller named character-count that updates a <span> with the current character count of a <textarea> every time its value changes, plus the HTML using data-controller/data-action/data-*-target to wire it up.

Goal: Practice connecting Stimulus targets and actions to existing server-rendered HTML.

→ Solution

💎 The Server Stays the Source of Truth

Every Hotwire technique in this chapter shares one property: the HTML the server renders is always correct, and the client's job is just to apply small, targeted patches to it — never to hold its own separate copy of the data that could drift out of sync. That's the core tradeoff against a full SPA framework, and the reason Hotwire fits so naturally on top of a framework already built around server-rendered convention (rails1-4's ERB views).

🎯 What's Next

Next chapter: Deployment — getting a Rails app onto Heroku, Render, or Docker, asset precompilation, and production database configuration, closing out this course.