skills/hotwire-patterns/SKILL.md
Analyzes and recommends Hotwire patterns including Stimulus controllers, Turbo Frames, Turbo Streams, ActionCable broadcasts, and progressive enhancement for Rails frontends. Use when building interactive UI, partial page updates, real-time features, or form enhancements. NOT for REST API JSON responses, GraphQL, server-only background jobs, or model/database design.
npx skillsauth add ag0os/rails-dev-plugin hotwire-patternsInstall this skill globally with one command. Works with Claude Code, Cursor, and Windsurf.
3 of 9 scanners reported clean
Some scanners were skipped, did not run, or reported a non-clean status. Review each row below.
Analyze and recommend Stimulus + Turbo patterns for modern, interactive Rails applications.
Applies on the html delivery axis (Axis B, rails-stack-profiles). An api-delivery project has no server-rendered views and does not use Hotwire — use rails-api-patterns instead.
| Component | Use When | |-----------|----------| | Stimulus | Adding JS behaviors to server-rendered HTML | | Turbo Drive | Default for all links/forms (no config needed) | | Turbo Frames | Update a section without full reload | | Turbo Streams | Update multiple DOM elements from one response | | ActionCable + Streams | Push real-time updates to connected clients |
HTML over the wire: Send HTML from the server, not JSON. JavaScript enhances server-rendered HTML.
| Scenario | Use | Why |
|----------|-----|-----|
| Edit-in-place | Frame | Scoped navigation, replaces itself |
| Form creates item + updates counter | Stream | Multiple targets from one response |
| Lazy sidebar | Frame with loading: :lazy | Deferred load, single target |
| Real-time chat | ActionCable + Stream | Push from server to all clients |
| Tabs/pagination | Frame | Scoped replacement |
| Flash + content update | Stream | Two targets: flash div + content |
This pattern is non-obvious -- it wires a Stimulus controller to an ActionCable subscription, giving you lifecycle-managed real-time behavior:
// app/javascript/controllers/chat_controller.js
import { Controller } from "@hotwired/stimulus"
import consumer from "../channels/consumer"
export default class extends Controller {
static targets = ["messages", "input"]
static values = { roomId: Number }
connect() {
this.subscription = consumer.subscriptions.create(
{ channel: "ChatChannel", room_id: this.roomIdValue },
{ received: (data) => this.messagesTarget.insertAdjacentHTML("beforeend", data.html) }
)
}
disconnect() { this.subscription?.unsubscribe() }
send(e) {
e.preventDefault()
if (this.inputTarget.value.trim()) {
this.subscription.send({ message: this.inputTarget.value })
this.inputTarget.value = ""
}
}
}
Key: disconnect() must unsubscribe to prevent leaked subscriptions during Turbo navigation.
Turbo will NOT render form error responses unless the server returns status: :unprocessable_entity (422). This is the #1 Hotwire debugging issue:
# WRONG: Turbo ignores this response
format.html { render :new }
# RIGHT: Turbo processes the response
format.html { render :new, status: :unprocessable_entity }
If the response HTML does not contain a <turbo-frame> with a matching id, nothing happens -- no error, no update. Debug with:
dom_id(@record) produces the same ID on both pages# WRONG: every connected user sees every message
after_create_commit { broadcast_prepend_to "messages" }
# RIGHT: scope to the relevant stream
after_create_commit -> { broadcast_prepend_to(room, :messages) }
after_create_commit -> { broadcast_prepend_to(user, :notifications) } # per-user
| Anti-Pattern | Problem | Fix |
|-------------|---------|-----|
| Client-side state management | Fights Hotwire's server-first model | Keep state on server, re-render HTML |
| Fat Stimulus controllers (100+ lines) | Hard to maintain | Extract into multiple focused controllers |
| Broadcasting without scoping | All users see all updates | Scope broadcasts to relevant streams |
| No loading: :lazy on hidden frames | Unnecessary requests on page load | Use lazy loading for below-fold content |
| Streams when a Frame suffices | Overcomplicated | Use Frames for single-target scoped nav |
Before shipping any Hotwire feature, verify:
data-turbo="false")data-turbo-permanent preserves media players, ActionCable connections across navigationdata-turbo="false" on file downloads and external linksWhen analyzing or creating Hotwire components, provide:
turbo_stream response formatdevelopment
WHAT: Language-agnostic corrective guidance for the refactoring phase. WHEN: Agent is restructuring code, fixing code smells, reducing complexity, or improving maintainability. NOT FOR: Writing new features, debugging runtime errors, performance tuning, or object design decisions.
tools
Analyzes Rails view templates, partials, layouts, helpers, and form patterns for best practices. Use when reviewing ERB templates, improving view performance with fragment caching, fixing form helpers, organizing partials, adding accessibility attributes, or evaluating collection rendering. NOT for Stimulus/Turbo logic (use hotwire-patterns), controller concerns, or API-only responses.
testing
Analyzes Rails test suites and recommends testing best practices for RSpec and Minitest. Use when writing new tests, reviewing test coverage, fixing flaky tests, improving test performance, choosing between test types (unit, integration, system, request), or setting up factories and fixtures. NOT for production monitoring, deployment verification, or load/stress testing infrastructure.
development
Detects a Rails project's architecture axes — logic placement (native vs extracted) and delivery (html vs api) — so other skills load profile-appropriate guidance without inline conditionals. Use when planning architecture or when a recommendation depends on where business logic lives or whether the app renders HTML or serves JSON. NOT for test framework, job backend, cache store, or auth library choices — those are orthogonal facts detected by project-conventions.