RoR Turbo and Hotwire
Hotwire is a collection of tools that makes Rails apps feel fast and interactive without writing JavaScript. It sends HTML over the wire instead of JSON and uses small JavaScript libraries to update only the parts of the page that changed. The result is a snappy, app-like experience built entirely in Rails, Ruby, and minimal JavaScript.
Hotwire Components
Hotwire | +-- Turbo Drive ← makes page navigation feel instant | +-- Turbo Frames ← update specific page sections without full reload | +-- Turbo Streams ← push real-time HTML updates from server to browser | +-- Stimulus ← small JavaScript for behaviour that HTML cannot do alone
Turbo Drive — Instant Page Navigation
Turbo Drive intercepts every link click and form submission. Instead of a full page reload, it fetches only the new page's <body> and swaps it in. Your navigation, sidebar, and header never reload. Pages feel instant.
No code required — Turbo Drive is on by default in Rails 7+.
Opt out for a specific link:
<%= link_to "Download PDF", report_path, data: { turbo: false } %>
Turbo Frames — Update Page Sections
A Turbo Frame is a named region of a page. When a link or form inside a frame fires, only that frame updates — the rest of the page stays untouched.
Wrap a section in a turbo-frame tag:
app/views/articles/index.html.erb
<%= turbo_frame_tag "articles-list" do %>
<% @articles.each do |article| %>
<div><%= article.title %></div>
<% end %>
<%= link_to "Next Page", articles_path(page: @page + 1) %>
<% end %>
When the user clicks "Next Page," Rails returns the next page's HTML. Turbo finds the matching turbo_frame_tag "articles-list" in the response and swaps only that section — no full page reload, no JavaScript code written.
Turbo Frames for Inline Editing
Show page:
app/views/articles/show.html.erb
<%= turbo_frame_tag @article do %>
<h1><%= @article.title %></h1>
<p><%= @article.body %></p>
<%= link_to "Edit Inline", edit_article_path(@article) %>
<% end %>
Edit page:
app/views/articles/edit.html.erb
<%= turbo_frame_tag @article do %>
<%= form_with model: @article do |f| %>
<%= f.text_field :title %>
<%= f.text_area :body %>
<%= f.submit "Save" %>
<% end %>
<% end %>
When the user clicks "Edit Inline," only the turbo frame swaps to the edit form. The rest of the page stays in place. After saving, the frame swaps back to the show view. Zero custom JavaScript.
Turbo Streams — Real-Time Updates
Turbo Streams push HTML updates from the server to the browser after actions. They can append, prepend, replace, remove, or update any element on the page by its DOM id.
app/controllers/comments_controller.rb
def create
@comment = @article.comments.build(comment_params)
@comment.user = current_user
respond_to do |format|
if @comment.save
format.turbo_stream ← renders the turbo_stream view
format.html { redirect_to @article }
else
format.html { render :new, status: :unprocessable_entity }
end
end
end
app/views/comments/create.turbo_stream.erb <%= turbo_stream.append "comments-list" do %> <%= render @comment %> <% end %> <%= turbo_stream.replace "comment-form" do %> <%= render "form", article: @article, comment: Comment.new %> <% end %>
The user submits a comment. Rails saves it, then sends back two Turbo Stream actions: append the new comment to the list, and reset the comment form. No page reload. No polling. No WebSocket setup. It just works.
Turbo Stream Actions
turbo_stream.append "dom-id" do ... end ← add inside element, at end turbo_stream.prepend "dom-id" do ... end ← add inside element, at start turbo_stream.replace "dom-id" do ... end ← swap the element turbo_stream.update "dom-id" do ... end ← update element's content turbo_stream.remove "dom-id" ← delete the element turbo_stream.before "dom-id" do ... end ← add before the element turbo_stream.after "dom-id" do ... end ← add after the element
Stimulus — JavaScript for Behaviour
Stimulus handles interactions that HTML cannot express — like toggling menus, adding keyboard shortcuts, or connecting to browser APIs. Stimulus controllers are small JavaScript classes that attach to HTML elements via data attributes:
app/javascript/controllers/dropdown_controller.js
import { Controller } from "@hotwired/stimulus"
export default class extends Controller {
static targets = ["menu"]
toggle() {
this.menuTarget.classList.toggle("hidden")
}
}
In your HTML:
<div data-controller="dropdown">
<button data-action="click->dropdown#toggle">
Menu ▼
</button>
<ul data-dropdown-target="menu" class="hidden">
<li><a href="/profile">Profile</a></li>
<li><a href="/settings">Settings</a></li>
</ul>
</div>
Hotwire vs React/Vue
| Factor | Hotwire | React / Vue |
|---|---|---|
| JavaScript written | Very little | A lot |
| Views | ERB (server-side) | JSX / components (client-side) |
| API needed | No | Yes (JSON API) |
| Learning curve | Low — standard Rails | High — full JS framework |
| Best for | Standard web apps with some interactivity | Highly interactive single-page apps |
Hotwire fits 80% of web application needs. Use it to add interactivity to your Rails app without abandoning the productivity of server-side Rails development. Reach for React or Vue only when you need a highly stateful, complex frontend experience.
