| name | hotwire-patterns |
| description | Hotwire in Rails: Turbo Frames, Streams, Stimulus, Action Cable. Triggers: "live update no reload", "broadcast model", "stimulus controller". Do NOT use for: native mobile, jobs. |
| user-invocable | false |
| effort | medium |
Hotwire Patterns
Iron Laws
- Keep server-rendered interactions simple before reaching for custom JS.
- Broadcast after commit when Turbo Stream updates depend on committed state.
- Keep Stimulus controllers focused on browser concerns, not business rules.
- Avoid duplicating the same state in DOM, session, cache, and
Current without a clear source of truth.
- Use Turbo Frames for scoped navigation; Turbo Streams for server-driven updates.
- Progressive enhancement: pages work without JavaScript, enhanced with it.
Overview
Hotwire provides tools for building modern web applications with minimal JavaScript:
- Turbo Drive — Handles page navigation without full reloads
- Turbo Frames — Scoped page updates, independent navigation
- Turbo Streams — Server-driven page changes via WebSocket or HTTP
- Stimulus — JavaScript controllers for progressive enhancement
Quick Decision Guide
| Use Case | Tool | Example |
|---|
| Page navigation | Turbo Drive | Click links, instant page loads |
| Modal/dialog | Turbo Frame | Login modal, settings panel |
| Independent scrollable area | Turbo Frame | Comments section, sidebar |
| Real-time updates | Turbo Streams | Notifications, live comments |
| Form validation | Stimulus | Character counter, date picker |
| Complex UI interaction | Stimulus | Drag and drop, autocomplete |
Turbo Frames
<!-- Frame that updates independently -->
<%= turbo_frame_tag "comments" do %>
<%= render @comments %>
<% end %>
<!-- Link targets specific frame -->
<%= link_to "Load more", comments_path(page: 2), data: { turbo_frame: "comments" } %>
See Turbo Architecture for Drive, Frames, and Streams.
Turbo Streams
<!-- Broadcast from model -->
<%= turbo_stream_from @post %>
<!-- In view, streams update automatically -->
<%= turbo_frame_tag dom_id(@post) do %>
<%= render @post %>
<% end %>
class CommentsController < ApplicationController
def create
@comment = @post.comments.create!(comment_params)
respond_to do |format|
format.turbo_stream
format.html { redirect_to @post }
end
end
end
See Real-time Features for broadcast patterns.
Stimulus Controllers
import { Controller } from "@hotwired/stimulus"
export default class extends Controller {
static targets = ["source", "button"]
copy() {
navigator.clipboard.writeText(this.sourceTarget.value)
this.buttonTarget.textContent = "Copied!"
setTimeout(() => {
this.buttonTarget.textContent = "Copy"
}, 2000)
}
}
<!-- Usage -->
<div data-controller="clipboard">
<input data-clipboard-target="source" value="Copy me!" readonly>
<button data-clipboard-target="button" data-action="click->clipboard#copy">
Copy
</button>
</div>
See Stimulus Controllers for patterns and best practices.
References
| Need | Reference |
|---|
| Turbo Drive / Frames / Streams architecture | ${CLAUDE_SKILL_DIR}/references/turbo-architecture.md |
| Stimulus controller patterns | ${CLAUDE_SKILL_DIR}/references/stimulus-controllers.md |
| no-JS-first / progressive enhancement | ${CLAUDE_SKILL_DIR}/references/progressive-enhancement.md |
| real-time / notification / live-update broadcast patterns | ${CLAUDE_SKILL_DIR}/references/realtime-features.md |
| Hotwire testing | ${CLAUDE_SKILL_DIR}/references/testing.md |
| Hotwire anti-patterns | ${CLAUDE_SKILL_DIR}/references/anti-patterns.md |
| Rails UJS → Hotwire migration | ${CLAUDE_SKILL_DIR}/references/migration-guide.md |
| Hotwire performance tips | ${CLAUDE_SKILL_DIR}/references/performance-tips.md |
| lazy-load Turbo Frames + infinite scroll + background-job streams | ${CLAUDE_SKILL_DIR}/references/async-streams.md |
| ActionCable channels + Turbo Streams + Redis presence tracking | ${CLAUDE_SKILL_DIR}/references/channels-presence.md |
| Turbo forms + Active Storage direct uploads + Stimulus debouncing | ${CLAUDE_SKILL_DIR}/references/forms-uploads.md |
data-turbo-permanent + Stimulus connect/disconnect for TipTap, Chart.js, Leaflet, Alpine | ${CLAUDE_SKILL_DIR}/references/js-interop.md |
| pubsub broadcast patterns + Turbo Drive / Frames / modal navigation | ${CLAUDE_SKILL_DIR}/references/pubsub-navigation.md |
| reusable Turbo components | ${CLAUDE_SKILL_DIR}/references/components.md |
Key Patterns
Frame + Stream Combo
<!-- Frame for navigation, stream for updates -->
<%= turbo_frame_tag "room" do %>
<%= turbo_stream_from @room %>
<div id="messages">
<%= render @room.messages %>
</div>
<% end %>
Lazy Loading Frames
<%= turbo_frame_tag "stats", src: stats_path, loading: :lazy do %>
<p>Loading stats...</p>
<% end %>
Stimulus + Turbo Events
connect() {
this.element.addEventListener("turbo:submit-end", () => {
this.resetForm()
})
}
See Also
Related — invoke manually if needed
- Request-state / session-leak hygiene check →
/rb:state-audit (request-state hygiene)
- iOS / Android Hotwire bridge work →
/rb:hotwire-native (native mobile Hotwire)