Hotwire — Turbo & Stimulus
🔥 Hotwire: Turbo & Stimulus
🚗 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
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
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.
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
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)
| Concern | React/Vue/Svelte | Hotwire |
|---|---|---|
| Rendering | Client-side, from a JS component tree | Server-side ERB, always |
| State | Explicit client state, synced to the server | Server is the only source of truth |
| Data fetching | Client calls a JSON API, re-renders | Server pushes/returns HTML fragments directly |
| JS footprint | The entire UI is JavaScript | Small, scoped behavior only (Stimulus) |
| Best fit | Rich, highly interactive client apps | Content-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.
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.
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.
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.