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.

Gids over Hotwire en Turbo voor Ruby on Rails 7

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.

Waarom Hotwire?

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.

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

Voor bestaande projecten vereist de installatie slechts een commando.

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"

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.

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 kan worden uitgeschakeld voor specifieke links of formulieren wanneer nodig.

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

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>

Het serverantwoord moet een frame bevatten met dezelfde identifier om de update te laten werken.

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 met Turbo Frames

Frames kunnen hun inhoud asynchroon laden met het src-attribuut. De initiele inhoud wordt weergegeven tijdens het laden.

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>

De controller antwoordt met een view die alleen het gevraagde frame bevat.

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>

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.

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

Speciale Turbo Stream Templates

Voor complexere antwoorden biedt een .turbo_stream.erb-template meer flexibiliteit.

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 %>
Antwoordformaat

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.

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

De view abonneert zich op de bijbehorende stream met de 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 %>

Asynchroon Broadcasting voor Zware Operaties

Om verzoeken niet te blokkeren, kan broadcasting op de achtergrond worden uitgevoerd.

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

Geavanceerde Patronen met Turbo

Inline Formulierbewerking

Een veelvoorkomend patroon vervangt statische inhoud door een inline bewerkingsformulier.

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>

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.

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 Debuggen

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.

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")
  }
}

Serverfoutafhandeling

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

Prestatie-Optimalisatie

Meerdere technieken zorgen voor performante Turbo-applicaties.

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 %>

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_to met format.turbo_stream in 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

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

Delen