Ruby on Rails 7: Hotwire en Turbo voor Reactieve Applicaties
Volledige gids over Hotwire en Turbo in Rails 7. Bouw reactieve applicaties zonder JavaScript met Turbo Drive, Frames en Streams.

Rails 7 heeft webontwikkeling gerevolutioneerd door Hotwire standaard te integreren. Deze stack maakt het mogelijk om zeer reactieve applicaties te bouwen zonder een enkele regel aangepast JavaScript te schrijven. Turbo Drive, Turbo Frames en Turbo Streams vervangen traditionele SPA-benaderingen met een "HTML over de draad"-filosofie.
Hotwire vermindert de frontend-complexiteit drastisch. Geen React of Vue nodig voor dynamische interfaces: de server stuurt kant-en-klaar HTML en Turbo regelt de DOM-updates automatisch.
De Hotwire-Architectuur Begrijpen
Hotwire bestaat uit drie complementaire technologieen die samenwerken om een vloeiende gebruikerservaring te bieden zonder de typische complexiteit van JavaScript-frameworks.
Turbo Drive versnelt de navigatie door klikken op links en formulierinzendingen te onderscheppen. In plaats van de hele pagina opnieuw te laden, wordt alleen de inhoud van de <body> vervangen, waarbij de JavaScript- en CSS-context behouden blijft.
Turbo Frames splitsen pagina's op in onafhankelijke secties. Elk frame kan apart worden bijgewerkt, waardoor gerichte interacties mogelijk zijn zonder de rest van de pagina te beinvloeden.
Turbo Streams maken realtime-updates mogelijk via WebSocket of als reactie op HTTP-verzoeken. Acht CRUD-acties zijn beschikbaar voor declaratieve DOM-manipulatie.
# Gemfile
# Installing Turbo Rails (included by default in Rails 7+)
gem 'turbo-rails'Voor bestaande projecten vereist de installatie slechts een commando.
# 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"Initiele Turbo Drive Configuratie
Turbo Drive is standaard ingeschakeld in Rails 7. Alle navigatie wordt automatisch "turbo" zonder extra configuratie. Het gedrag kan worden aangepast via data-attributen.
<!-- 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 kan worden uitgeschakeld voor specifieke links of formulieren wanneer nodig.
<!-- 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 cachet bezochte pagina's. Het attribuut data-turbo-track="reload" op assets forceert een volledige herlaadbeurt als CSS/JS-bestanden wijzigen.
Turbo Frames voor Gerichte Updates
Turbo Frames definieren paginazones die onafhankelijk worden bijgewerkt. Elk frame heeft een uniek identificatienummer en reageert alleen op antwoorden die een overeenkomend frame bevatten.
<!-- 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>Het serverantwoord moet een frame bevatten met dezelfde identifier om de update te laten werken.
<!-- 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 met Turbo Frames
Frames kunnen hun inhoud asynchroon laden met het src-attribuut. De initiele inhoud wordt weergegeven tijdens het laden.
<!-- 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>De controller antwoordt met een view die alleen het gevraagde frame bevat.
# 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>Klaar om je Ruby on Rails gesprekken te halen?
Oefen met onze interactieve simulatoren, flashcards en technische tests.
Turbo Streams voor Realtime Updates
Turbo Streams biedt acht acties voor DOM-manipulatie: append, prepend, replace, update, remove, before, after en morph. Deze acties kunnen worden geactiveerd via HTTP-antwoord of WebSocket.
# 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
endSpeciale Turbo Stream Templates
Voor complexere antwoorden biedt een .turbo_stream.erb-template meer flexibiliteit.
<!-- 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-antwoorden moeten het content-type text/vnd.turbo-stream.html hebben. Rails handelt dit automatisch af met respond_to en het turbo_stream-formaat.
Realtime Broadcasting met Action Cable
Turbo Streams schittert echt bij WebSocket-broadcasting. Gebruikers ontvangen updates direct zonder 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
endDe view abonneert zich op de bijbehorende stream met de 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 %>Asynchroon Broadcasting voor Zware Operaties
Om verzoeken niet te blokkeren, kan broadcasting op de achtergrond worden uitgevoerd.
# 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
endGeavanceerde Patronen met Turbo
Inline Formulierbewerking
Een veelvoorkomend patroon vervangt statische inhoud door een inline bewerkingsformulier.
<!-- 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>Navigatie Buiten het Bovenliggende Frame
Standaard blijven links in een frame binnen dat frame. Het data-turbo-frame-attribuut maakt het mogelijk om een ander frame of de hele pagina aan te spreken.
<!-- 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>Open in de ontwikkelomgeving de JavaScript-console en typ Turbo.setProgressBarDelay(0) om de voortgangsbalk direct te zien. Turbo.session.drive = false schakelt Turbo Drive tijdelijk uit.
Foutafhandeling en Laadstatussen
Een goede UX vereist het afhandelen van laadstatussen en netwerkfouten.
<!-- 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")
}
}Serverfoutafhandeling
# 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
endPrestatie-Optimalisatie
Meerdere technieken zorgen voor performante Turbo-applicaties.
# 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 %>Conclusie
Hotwire en Turbo transformeren Rails-ontwikkeling door de complexiteit van traditionele JavaScript-frameworks te elimineren. Turbo Drive versnelt de navigatie, Turbo Frames maken gerichte updates mogelijk, en Turbo Streams biedt krachtige realtime-mogelijkheden.
Checklist om te Beginnen met Hotwire
- Rails 7+ gebruiken voor ingebouwde Hotwire-integratie
- De drie componenten begrijpen: Drive, Frames en Streams
- Paginazones identificeren die baat hebben bij Turbo Frames
respond_tometformat.turbo_streamin controllers gebruiken- Broadcasting implementeren voor realtime-functionaliteit
- Combineren met Stimulus voor eenvoudige JavaScript-interacties
Begin met oefenen!
Test je kennis met onze gespreksimulatoren en technische tests.
Deze "HTML over de draad"-benadering maakt het mogelijk om moderne, reactieve applicaties te bouwen met behoud van de eenvoud en productiviteit die Ruby on Rails zo krachtig maken. Het resultaat: minder code om te onderhouden, uitstekende prestaties en een optimale ontwikkelaarservaring.
Tags
Delen