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.

Leitfaden zu Hotwire und Turbo fuer Ruby on Rails 7

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.

Warum Hotwire?

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.

ruby
# Gemfile
# Installing Turbo Rails (included by default in Rails 7+)
gem 'turbo-rails'

Fuer bestehende Projekte erfordert die Installation nur einen Befehl.

bash
# 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.

erb
<!-- 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.

erb
<!-- 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 Cache

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.

erb
<!-- 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.

erb
<!-- 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.

erb
<!-- 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.

ruby
# 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
erb
<!-- 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.

ruby
# 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
end

Dedizierte Turbo Stream Templates

Fuer komplexere Antworten bietet ein .turbo_stream.erb-Template mehr Flexibilitaet.

erb
<!-- 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 %>
Antwortformat

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.

ruby
# 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
end

Die View abonniert den entsprechenden Stream mit dem turbo_stream_from-Helper.

erb
<!-- 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.

ruby
# 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
ruby
# 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
end

Fortgeschrittene Muster mit Turbo

Inline-Formular-Bearbeitung

Ein gaengiges Muster ersetzt statischen Inhalt durch ein Inline-Bearbeitungsformular.

erb
<!-- 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>
erb
<!-- 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>

Standardmaessig bleiben Links in einem Frame innerhalb dieses Frames. Das Attribut data-turbo-frame ermoeglicht es, einen anderen Frame oder die gesamte Seite anzusprechen.

erb
<!-- 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>
erb
<!-- 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>
Turbo Debugging

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.

erb
<!-- 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 %>
app/javascript/controllers/form_loading_controller.jsjavascript
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

ruby
# 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
end

Performance-Optimierung

Mehrere Techniken stellen performante Turbo-Anwendungen sicher.

ruby
# 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
erb
<!-- 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_to mit format.turbo_stream in 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

#ruby on rails
#hotwire
#turbo
#turbo frames
#turbo streams

Teilen