Ruby on Rails 7: Hotwire und Turbo fuer reaktive Anwendungen
Vollstaendiger Leitfaden zu Hotwire und Turbo in Rails 7. Reaktive Anwendungen ohne JavaScript mit Turbo Drive, Frames und Streams entwickeln.

Rails 7 hat die Webentwicklung revolutioniert, indem Hotwire standardmaessig integriert wurde. Dieser Stack ermoeglicht es, hochreaktive Anwendungen zu erstellen, ohne eine einzige Zeile benutzerdefinierten JavaScript-Code zu schreiben. Turbo Drive, Turbo Frames und Turbo Streams ersetzen traditionelle SPA-Ansaetze durch eine "HTML ueber den Draht"-Philosophie.
Hotwire reduziert die Frontend-Komplexitaet drastisch. Kein React oder Vue fuer dynamische Oberflaechen noetig: Der Server sendet gebrauchsfertiges HTML, und Turbo uebernimmt die DOM-Aktualisierungen automatisch.
Die Hotwire-Architektur verstehen
Hotwire besteht aus drei komplementaeren Technologien, die zusammenarbeiten, um eine fluessige Benutzererfahrung ohne die typische JavaScript-Framework-Komplexitaet zu bieten.
Turbo Drive beschleunigt die Navigation, indem Link-Klicks und Formularuebermittlungen abgefangen werden. Statt die gesamte Seite neu zu laden, wird nur der Inhalt des <body> ersetzt, wobei der JavaScript- und CSS-Kontext erhalten bleibt.
Turbo Frames zerlegen Seiten in unabhaengige Abschnitte. Jeder Frame kann separat aktualisiert werden, was gezielte Interaktionen ermoeglicht, ohne den Rest der Seite zu beeinflussen.
Turbo Streams ermoeglichen Echtzeit-Aktualisierungen ueber WebSocket oder als Antwort auf HTTP-Anfragen. Acht CRUD-Aktionen stehen fuer deklarative DOM-Manipulation zur Verfuegung.
# Gemfile
# Installing Turbo Rails (included by default in Rails 7+)
gem 'turbo-rails'Fuer bestehende Projekte erfordert die Installation nur einen Befehl.
# installation.sh
# Installing Turbo in an existing Rails project
rails turbo:install
# Verify the JavaScript import is present
cat app/javascript/application.js
# Should contain: import "@hotwired/turbo-rails"Initiale Turbo Drive Konfiguration
Turbo Drive ist in Rails 7 standardmaessig aktiviert. Alle Navigation wird automatisch zu "Turbo", ohne zusaetzliche Konfiguration. Das Verhalten kann ueber Data-Attribute angepasst werden.
<!-- app/views/layouts/application.html.erb -->
<!DOCTYPE html>
<html>
<head>
<meta name="turbo-cache-control" content="no-preview">
<%= csrf_meta_tags %>
<%= csp_meta_tag %>
<%= stylesheet_link_tag "application", "data-turbo-track": "reload" %>
<%= javascript_importmap_tags %>
</head>
<body>
<!-- Progress bar during navigation -->
<div class="turbo-progress-bar"></div>
<%= yield %>
</body>
</html>Turbo Drive kann bei Bedarf fuer bestimmte Links oder Formulare deaktiviert werden.
<!-- app/views/pages/examples.html.erb -->
<!-- Link with Turbo enabled (default) -->
<%= link_to "Dashboard", dashboard_path %>
<!-- Disable Turbo for a specific link -->
<%= link_to "Download PDF", export_path, data: { turbo: false } %>
<!-- Disable Turbo for a form (file upload for example) -->
<%= form_with model: @document, data: { turbo: false } do |f| %>
<%= f.file_field :attachment %>
<%= f.submit "Upload" %>
<% end %>
<!-- Turbo with confirmation -->
<%= link_to "Delete", item_path(@item),
data: { turbo_method: :delete, turbo_confirm: "Are you sure?" } %>Turbo Drive speichert besuchte Seiten im Cache. Das Attribut data-turbo-track="reload" bei Assets erzwingt ein vollstaendiges Neuladen, wenn sich CSS/JS-Dateien aendern.
Turbo Frames fuer gezielte Aktualisierungen
Turbo Frames definieren Seitenbereiche, die unabhaengig aktualisiert werden. Jeder Frame hat einen eindeutigen Bezeichner und reagiert nur auf Antworten, die einen passenden Frame enthalten.
<!-- app/views/messages/index.html.erb -->
<h1>Messages</h1>
<!-- Frame for the message list -->
<turbo-frame id="messages_list">
<div id="messages">
<%= render @messages %>
</div>
<%= link_to "Load more", messages_path(page: @next_page) %>
</turbo-frame>
<!-- Frame for the creation form -->
<turbo-frame id="new_message">
<%= render "form", message: Message.new %>
</turbo-frame>Die Serverantwort muss einen Frame mit demselben Bezeichner enthalten, damit die Aktualisierung funktioniert.
<!-- app/views/messages/_message.html.erb -->
<!-- Partial for an individual message -->
<turbo-frame id="<%= dom_id(message) %>">
<div class="message">
<p><%= message.content %></p>
<span class="metadata">
By <%= message.author.name %> - <%= time_ago_in_words(message.created_at) %>
</span>
<!-- These links stay within the frame -->
<%= link_to "Edit", edit_message_path(message) %>
<%= button_to "Delete", message_path(message), method: :delete %>
</div>
</turbo-frame>Lazy Loading mit Turbo Frames
Frames koennen ihren Inhalt asynchron ueber das src-Attribut laden. Der initiale Inhalt wird waehrend des Ladens angezeigt.
<!-- app/views/dashboard/show.html.erb -->
<h1>Dashboard</h1>
<!-- Statistics loaded asynchronously -->
<turbo-frame id="stats" src="<%= dashboard_stats_path %>">
<div class="loading-placeholder">
<p>Loading statistics...</p>
</div>
</turbo-frame>
<!-- Notifications loaded when visible (lazy loading) -->
<turbo-frame id="notifications"
src="<%= notifications_path %>"
loading="lazy">
<p>Loading notifications...</p>
</turbo-frame>
<!-- Recent activity with periodic refresh -->
<turbo-frame id="recent_activity"
src="<%= activity_path %>"
data-controller="auto-refresh"
data-auto-refresh-interval-value="30000">
<%= render "activity/placeholder" %>
</turbo-frame>Der Controller antwortet mit einer View, die nur den angeforderten Frame enthaelt.
# app/controllers/dashboard_controller.rb
class DashboardController < ApplicationController
def stats
@user_count = User.count
@message_count = Message.count
@active_today = User.where("last_seen_at > ?", 24.hours.ago).count
# Partial rendering for the frame
render partial: "dashboard/stats"
end
end<!-- app/views/dashboard/_stats.html.erb -->
<turbo-frame id="stats">
<div class="stats-grid">
<div class="stat-card">
<h3><%= @user_count %></h3>
<p>Users</p>
</div>
<div class="stat-card">
<h3><%= @message_count %></h3>
<p>Messages</p>
</div>
<div class="stat-card">
<h3><%= @active_today %></h3>
<p>Active today</p>
</div>
</div>
</turbo-frame>Bereit für deine Ruby on Rails-Interviews?
Übe mit unseren interaktiven Simulatoren, Flashcards und technischen Tests.
Turbo Streams fuer Echtzeit-Aktualisierungen
Turbo Streams bieten acht Aktionen zur DOM-Manipulation: append, prepend, replace, update, remove, before, after und morph. Diese Aktionen koennen ueber HTTP-Antwort oder WebSocket ausgeloest werden.
# app/controllers/messages_controller.rb
class MessagesController < ApplicationController
def create
@message = current_user.messages.build(message_params)
respond_to do |format|
if @message.save
# Turbo Stream response to add the message
format.turbo_stream do
render turbo_stream: [
turbo_stream.prepend("messages", @message),
turbo_stream.update("message_count", partial: "messages/count"),
turbo_stream.replace("new_message", partial: "messages/form",
locals: { message: Message.new })
]
end
format.html { redirect_to messages_path, notice: "Message created" }
else
format.turbo_stream do
render turbo_stream: turbo_stream.replace(
"new_message",
partial: "messages/form",
locals: { message: @message }
)
end
format.html { render :new, status: :unprocessable_entity }
end
end
end
def destroy
@message = Message.find(params[:id])
@message.destroy
respond_to do |format|
# Removal with animation
format.turbo_stream { render turbo_stream: turbo_stream.remove(@message) }
format.html { redirect_to messages_path, notice: "Message deleted" }
end
end
endDedizierte Turbo Stream Templates
Fuer komplexere Antworten bietet ein .turbo_stream.erb-Template mehr Flexibilitaet.
<!-- app/views/messages/create.turbo_stream.erb -->
<!-- Prepend the new message to the list -->
<%= turbo_stream.prepend "messages" do %>
<%= render @message %>
<% end %>
<!-- Update the counter -->
<%= turbo_stream.update "message_count" do %>
<span><%= Message.count %> messages</span>
<% end %>
<!-- Reset the form -->
<%= turbo_stream.replace "new_message" do %>
<%= render "form", message: Message.new %>
<% end %>
<!-- Display a flash notification -->
<%= turbo_stream.prepend "flash_messages" do %>
<div class="flash flash-success" data-controller="auto-dismiss">
Message sent successfully!
</div>
<% end %>Turbo Stream-Antworten muessen den Content-Type text/vnd.turbo-stream.html haben. Rails handhabt dies automatisch mit respond_to und dem turbo_stream-Format.
Echtzeit-Broadcasting mit Action Cable
Turbo Streams entfalten ihre volle Staerke beim WebSocket-Broadcasting. Benutzer erhalten Aktualisierungen sofort ohne Polling.
# app/models/message.rb
class Message < ApplicationRecord
belongs_to :room
belongs_to :author, class_name: "User"
# Automatic broadcast after creation
after_create_commit do
broadcast_append_to(
room,
target: "messages",
partial: "messages/message",
locals: { message: self }
)
end
# Broadcast after update
after_update_commit do
broadcast_replace_to(
room,
target: dom_id(self),
partial: "messages/message",
locals: { message: self }
)
end
# Broadcast after deletion
after_destroy_commit do
broadcast_remove_to(room, target: dom_id(self))
end
endDie View abonniert den entsprechenden Stream mit dem turbo_stream_from-Helper.
<!-- app/views/rooms/show.html.erb -->
<h1><%= @room.name %></h1>
<!-- Subscribe to the WebSocket stream -->
<%= turbo_stream_from @room %>
<!-- Container for messages -->
<div id="messages">
<%= render @room.messages.order(created_at: :asc) %>
</div>
<!-- New message form -->
<%= form_with model: [@room, Message.new], id: "new_message" do |f| %>
<%= f.text_area :content, placeholder: "Your message..." %>
<%= f.submit "Send" %>
<% end %>Asynchrones Broadcasting fuer aufwendige Operationen
Um Anfragen nicht zu blockieren, kann das Broadcasting im Hintergrund durchgefuehrt werden.
# app/models/report.rb
class Report < ApplicationRecord
belongs_to :user
after_create_commit :generate_async
private
def generate_async
GenerateReportJob.perform_later(self)
end
end# app/jobs/generate_report_job.rb
class GenerateReportJob < ApplicationJob
queue_as :default
def perform(report)
# Simulating a long operation
report.update!(status: "processing")
# Notify user of start
report.broadcast_replace_to(
report.user, :reports,
target: dom_id(report),
partial: "reports/report"
)
# Generate the report
result = ReportGenerator.new(report).generate
report.update!(content: result, status: "completed")
# Notify user of completion
report.broadcast_replace_to(
report.user, :reports,
target: dom_id(report),
partial: "reports/report"
)
end
endFortgeschrittene Muster mit Turbo
Inline-Formular-Bearbeitung
Ein gaengiges Muster ersetzt statischen Inhalt durch ein Inline-Bearbeitungsformular.
<!-- app/views/tasks/_task.html.erb -->
<turbo-frame id="<%= dom_id(task) %>">
<div class="task">
<span class="task-content"><%= task.content %></span>
<div class="task-actions">
<%= link_to "Edit", edit_task_path(task) %>
<%= button_to "Delete", task_path(task), method: :delete,
data: { turbo_confirm: "Delete this task?" } %>
</div>
</div>
</turbo-frame><!-- app/views/tasks/edit.html.erb -->
<turbo-frame id="<%= dom_id(@task) %>">
<%= form_with model: @task do |f| %>
<%= f.text_field :content, autofocus: true %>
<div class="form-actions">
<%= f.submit "Save" %>
<%= link_to "Cancel", task_path(@task) %>
</div>
<% end %>
</turbo-frame>Navigation ausserhalb des uebergeordneten Frames
Standardmaessig bleiben Links in einem Frame innerhalb dieses Frames. Das Attribut data-turbo-frame ermoeglicht es, einen anderen Frame oder die gesamte Seite anzusprechen.
<!-- app/views/search/_results.html.erb -->
<turbo-frame id="search_results">
<ul>
<% @results.each do |result| %>
<li>
<!-- This link navigates the entire page, not the frame -->
<%= link_to result.title, result_path(result),
data: { turbo_frame: "_top" } %>
</li>
<% end %>
</ul>
</turbo-frame><!-- app/views/layouts/_sidebar.html.erb -->
<aside>
<!-- This frame loads content and updates main -->
<turbo-frame id="sidebar_nav" target="main_content">
<%= render "navigation" %>
</turbo-frame>
</aside>
<main>
<turbo-frame id="main_content">
<%= yield %>
</turbo-frame>
</main>In der Entwicklungsumgebung die JavaScript-Konsole oeffnen und Turbo.setProgressBarDelay(0) eingeben, um die Fortschrittsleiste sofort zu sehen. Turbo.session.drive = false deaktiviert Turbo Drive voruebergehend.
Fehlerbehandlung und Ladezustaende
Eine gute UX erfordert die Behandlung von Ladezustaenden und Netzwerkfehlern.
<!-- app/views/shared/_form_with_loading.html.erb -->
<%= form_with model: @model,
data: {
controller: "form-loading",
action: "turbo:submit-start->form-loading#disable turbo:submit-end->form-loading#enable"
} do |f| %>
<%= yield f %>
<button type="submit" data-form-loading-target="submit">
<span data-form-loading-target="text">Save</span>
<span data-form-loading-target="spinner" class="hidden">
<svg class="animate-spin h-5 w-5"><!-- spinner SVG --></svg>
</span>
</button>
<% end %>import { Controller } from "@hotwired/stimulus"
export default class extends Controller {
static targets = ["submit", "text", "spinner"]
disable() {
this.submitTarget.disabled = true
this.textTarget.classList.add("hidden")
this.spinnerTarget.classList.remove("hidden")
}
enable() {
this.submitTarget.disabled = false
this.textTarget.classList.remove("hidden")
this.spinnerTarget.classList.add("hidden")
}
}Serverfehlerbehandlung
# app/controllers/application_controller.rb
class ApplicationController < ActionController::Base
rescue_from ActiveRecord::RecordNotFound do |exception|
respond_to do |format|
format.turbo_stream do
render turbo_stream: turbo_stream.replace(
"main_content",
partial: "shared/not_found"
)
end
format.html { render "errors/not_found", status: :not_found }
end
end
endPerformance-Optimierung
Mehrere Techniken stellen performante Turbo-Anwendungen sicher.
# app/controllers/messages_controller.rb
class MessagesController < ApplicationController
# Avoid N+1 queries with eager loading
def index
@messages = Message.includes(:author, :room)
.order(created_at: :desc)
.page(params[:page])
end
# Fragment caching for static elements
def show
@message = Message.find(params[:id])
fresh_when @message
end
end<!-- app/views/messages/_message.html.erb -->
<!-- Partial-level caching -->
<% cache message do %>
<turbo-frame id="<%= dom_id(message) %>">
<div class="message">
<%= message.content %>
<small>By <%= message.author.name %></small>
</div>
</turbo-frame>
<% end %>Fazit
Hotwire und Turbo transformieren die Rails-Entwicklung, indem sie die Komplexitaet traditioneller JavaScript-Frameworks eliminieren. Turbo Drive beschleunigt die Navigation, Turbo Frames ermoeglichen gezielte Aktualisierungen, und Turbo Streams bieten leistungsstarke Echtzeit-Faehigkeiten.
Checkliste fuer den Einstieg mit Hotwire
- Rails 7+ fuer integrierte Hotwire-Unterstuetzung verwenden
- Die drei Komponenten verstehen: Drive, Frames und Streams
- Seitenbereiche identifizieren, die von Turbo Frames profitieren
respond_tomitformat.turbo_streamin Controllern verwenden- Broadcasting fuer Echtzeit-Features implementieren
- Mit Stimulus fuer einfache JavaScript-Interaktionen kombinieren
Fang an zu üben!
Teste dein Wissen mit unseren Interview-Simulatoren und technischen Tests.
Dieser "HTML ueber den Draht"-Ansatz ermoeglicht es, moderne, reaktive Anwendungen zu erstellen und gleichzeitig die Einfachheit und Produktivitaet beizubehalten, die Ruby on Rails so leistungsstark machen. Das Ergebnis: weniger Code zu warten, hervorragende Performance und eine optimale Entwicklererfahrung.
Tags
Teilen