Back to list
Kaakati

turbo-hotwire-patterns

by Kaakati

Enterprise-grade Rails development workflow with multi-agent orchestration, beads task tracking, and quality gates. Generic and portable across any Rails project.

2🍴 0📅 Jan 24, 2026

SKILL.md


Turbo & Hotwire Patterns

Hotwire Decision Tree

What do I need?
│
├─ Full page navigation without reload?
│   └─ Turbo Drive (automatic, no config needed)
│
├─ Update part of page on interaction?
│   └─ Turbo Frames
│       └─ Wrap section in turbo_frame_tag
│
├─ Real-time updates from server?
│   └─ Turbo Streams + ActionCable
│       └─ Model broadcasts + turbo_stream_from
│
├─ Multiple DOM changes on form submit?
│   └─ Turbo Stream responses
│       └─ respond_to format.turbo_stream
│
├─ JavaScript behavior (click, input, etc)?
│   └─ Stimulus controller
│       └─ data-controller + data-action
│
└─ Communication between controllers?
    └─ Stimulus Outlets
        └─ static outlets + data-*-outlet

NEVER Do This

NEVER forget matching frame IDs:

<%# WRONG - IDs don't match, frame won't update %>
<%= turbo_frame_tag "tasks" do %>
  <%= link_to "Edit", edit_task_path(@task) %>
<% end %>

<%# edit.html.erb - different ID %>
<%= turbo_frame_tag "task_edit" do %>
  <%= render "form" %>
<% end %>

<%# RIGHT - IDs match %>
<%= turbo_frame_tag dom_id(@task) do %>
  <%= link_to "Edit", edit_task_path(@task) %>
<% end %>

<%# edit.html.erb - same ID %>
<%= turbo_frame_tag dom_id(@task) do %>
  <%= render "form" %>
<% end %>

NEVER return HTML for form errors (return 422):

# WRONG - 200 status doesn't show errors properly
format.turbo_stream {
  render turbo_stream: turbo_stream.replace("form", partial: "form")
}

# RIGHT - 422 status for validation errors
format.turbo_stream {
  render turbo_stream: turbo_stream.replace("form", partial: "form"),
         status: :unprocessable_entity
}

NEVER use target without checking existence:

// WRONG - crashes if target missing
search() {
  this.resultsTarget.innerHTML = html
}

// RIGHT - check first
search() {
  if (this.hasResultsTarget) {
    this.resultsTarget.innerHTML = html
  }
}

NEVER forget cleanup in disconnect:

// WRONG - memory leak
connect() {
  this.timer = setInterval(() => this.refresh(), 1000)
}

// RIGHT - clean up
connect() {
  this.timer = setInterval(() => this.refresh(), 1000)
}

disconnect() {
  clearInterval(this.timer)
}

NEVER skip ARIA for dynamic content:

<%# WRONG - screen readers miss updates %>
<div id="tasks">
  <%= render @tasks %>
</div>

<%# RIGHT - announce updates %>
<div id="tasks" aria-live="polite">
  <%= render @tasks %>
</div>

Hotwire Stack Overview

Hotwire
├── Turbo
│   ├── Turbo Drive      — Full page navigation without reload
│   ├── Turbo Frames     — Partial page updates
│   └── Turbo Streams    — Real-time updates over WebSocket/HTTP
│
└── Stimulus             — Lightweight JavaScript controllers

External References:


Turbo Drive

Automatically converts links/forms to AJAX. Disable when needed:

<%# Skip Turbo Drive for this link %>
<%= link_to "External", "https://example.com", data: { turbo: false } %>

<%# Skip for form %>
<%= form_with model: @user, data: { turbo: false } do |f| %>

Turbo Frames Quick Reference

PatternUsage
Basic frameturbo_frame_tag "id"
Model frameturbo_frame_tag dom_id(@task)
Target otherdata: { turbo_frame: "other_id" }
Break outdata: { turbo_frame: "_top" }
Lazy loadsrc: path, loading: :lazy
<%# Lazy loading example %>
<%= turbo_frame_tag "comments",
                    src: task_comments_path(@task),
                    loading: :lazy do %>
  <p>Loading comments...</p>
<% end %>

Turbo Streams Quick Reference

ActionResult
appendAdd to end of container
prependAdd to start of container
replaceReplace entire element
updateReplace element's contents
removeDelete element
before/afterInsert adjacent
<%# Stream response %>
<%= turbo_stream.prepend "tasks" do %>
  <%= render @task %>
<% end %>

<%= turbo_stream.remove dom_id(@deleted_task) %>

Model Broadcasts

class Task < ApplicationRecord
  after_create_commit -> { broadcast_prepend_to "tasks" }
  after_update_commit -> { broadcast_replace_to "tasks" }
  after_destroy_commit -> { broadcast_remove_to "tasks" }
end
<%# Subscribe in view %>
<%= turbo_stream_from "tasks" %>
<div id="tasks"><%= render @tasks %></div>

Stimulus Quick Reference

FeatureDeclarationHTML
Targetsstatic targets = ["input"]data-search-target="input"
Valuesstatic values = { delay: Number }data-search-delay-value="300"
Classesstatic classes = ["active"]data-search-active-class="bg-blue"
Actions-data-action="click->search#submit"
Outletsstatic outlets = ["form"]data-search-form-outlet="#form"

Basic Controller

import { Controller } from "@hotwired/stimulus"

export default class extends Controller {
  static targets = ["input", "results"]
  static values = { url: String }

  search() {
    fetch(`${this.urlValue}?q=${this.inputTarget.value}`)
      .then(r => r.text())
      .then(html => this.resultsTarget.innerHTML = html)
  }
}
<div data-controller="search"
     data-search-url-value="<%= search_path %>">
  <input data-search-target="input"
         data-action="input->search#search">
  <div data-search-target="results"></div>
</div>

Action Modifiers

ModifierEffect
:preventevent.preventDefault()
:stopevent.stopPropagation()
.enterOnly on Enter key
.escOnly on Escape key
.awayClick outside element
<form data-action="submit->form#save:prevent">
  <input data-action="keydown.enter->form#save:prevent">
</form>

Debugging Checklist

IssueCheck
Frame not updatingFrame IDs match?
Streams not workingturbo_stream_from subscription?
Actions not firingdata-action syntax correct? Controller registered?
Morphing issuesdata-turbo-permanent on persistent elements?

References

Detailed patterns and examples in references/:

  • turbo-frames.md - Frame patterns, lazy loading, navigation
  • turbo-streams.md - Stream actions, broadcasts, form validation
  • stimulus-controllers.md - Targets, values, actions, classes, outlets
  • common-patterns.md - Infinite scroll, auto-submit, flash messages
  • accessibility.md - ARIA, keyboard navigation, focus management
  • testing.md - System tests, Stimulus controller tests
  • turbo8-native.md - Turbo 8 morphing, native apps

Score

Total Score

60/100

Based on repository quality metrics

SKILL.md

SKILL.mdファイルが含まれている

+20
LICENSE

ライセンスが設定されている

0/10
説明文

100文字以上の説明がある

+10
人気

GitHub Stars 100以上

0/15
最近の活動

3ヶ月以内に更新がある

0/10
フォーク

10回以上フォークされている

0/5
Issue管理

オープンIssueが50未満

+5
言語

プログラミング言語が設定されている

+5
タグ

1つ以上のタグが設定されている

0/5

Reviews

💬

Reviews coming soon