Ruby on Rails 7: Hotwire e Turbo per Applicazioni Reattive

Guida completa a Hotwire e Turbo in Rails 7. Costruire applicazioni reattive senza scrivere JavaScript con Turbo Drive, Frames e Streams.

Guida a Hotwire e Turbo per Ruby on Rails 7

Rails 7 ha rivoluzionato lo sviluppo web integrando Hotwire di default. Questo stack consente di costruire applicazioni altamente reattive senza scrivere una singola riga di JavaScript personalizzato. Turbo Drive, Turbo Frames e Turbo Streams sostituiscono gli approcci SPA tradizionali con una filosofia "HTML via cavo".

Perche Hotwire?

Hotwire riduce drasticamente la complessita del frontend. Non servono React o Vue per interfacce dinamiche: il server invia HTML pronto all'uso e Turbo gestisce automaticamente gli aggiornamenti del DOM.

Comprendere l'Architettura di Hotwire

Hotwire consiste in tre tecnologie complementari che lavorano insieme per offrire un'esperienza utente fluida senza la tipica complessita dei framework JavaScript.

Turbo Drive accelera la navigazione intercettando i clic sui link e gli invii dei form. Invece di ricaricare l'intera pagina, viene sostituito solo il contenuto del <body>, preservando il contesto JavaScript e CSS.

Turbo Frames scompongono le pagine in sezioni indipendenti. Ogni frame puo essere aggiornato separatamente, consentendo interazioni mirate senza influire sul resto della pagina.

Turbo Streams consentono aggiornamenti in tempo reale tramite WebSocket o in risposta a richieste HTTP. Otto azioni CRUD sono disponibili per la manipolazione dichiarativa del DOM.

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

Per i progetti esistenti, l'installazione richiede un solo comando.

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"

Configurazione Iniziale di Turbo Drive

Turbo Drive e abilitato di default in Rails 7. Tutta la navigazione diventa automaticamente "turbo" senza configurazione aggiuntiva. Il comportamento puo essere personalizzato tramite attributi data.

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 puo essere disabilitato su link o form specifici quando necessario.

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?" } %>
Cache di Turbo Drive

Turbo Drive memorizza nella cache le pagine visitate. L'attributo data-turbo-track="reload" sugli asset forza un ricaricamento completo se i file CSS/JS cambiano.

Turbo Frames per Aggiornamenti Mirati

I Turbo Frames definiscono zone della pagina che si aggiornano in modo indipendente. Ogni frame ha un identificatore univoco e risponde solo alle risposte contenenti un frame corrispondente.

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>

La risposta del server deve contenere un frame con lo stesso identificatore affinche l'aggiornamento funzioni.

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>

Caricamento Lazy con Turbo Frames

I frame possono caricare il loro contenuto in modo asincrono usando l'attributo src. Il contenuto iniziale viene visualizzato durante il caricamento.

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>

Il controller risponde con una view contenente solo il frame richiesto.

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>

Pronto a superare i tuoi colloqui su Ruby on Rails?

Pratica con i nostri simulatori interattivi, flashcards e test tecnici.

Turbo Streams per Aggiornamenti in Tempo Reale

Turbo Streams offre otto azioni per la manipolazione del DOM: append, prepend, replace, update, remove, before, after e morph. Queste azioni possono essere attivate tramite risposta HTTP o 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

Template Dedicati per Turbo Stream

Per risposte piu complesse, un template .turbo_stream.erb offre maggiore flessibilita.

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 %>
Formato della Risposta

Le risposte Turbo Stream devono avere il content-type text/vnd.turbo-stream.html. Rails gestisce questo automaticamente con respond_to e il formato turbo_stream.

Broadcasting in Tempo Reale con Action Cable

Turbo Streams eccelle davvero con il broadcasting via WebSocket. Gli utenti ricevono aggiornamenti istantaneamente senza 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

La view si iscrive allo stream corrispondente con l'helper turbo_stream_from.

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

Broadcasting Asincrono per Operazioni Pesanti

Per evitare di bloccare le richieste, il broadcasting puo essere eseguito in background.

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

Pattern Avanzati con Turbo

Modifica Inline dei Form

Un pattern comune consiste nel sostituire il contenuto statico con un form di modifica inline.

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>

Per impostazione predefinita, i link in un frame rimangono all'interno di quel frame. L'attributo data-turbo-frame consente di puntare a un altro frame o all'intera pagina.

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>
Debug di Turbo

In sviluppo, aprire la console JavaScript e digitare Turbo.setProgressBarDelay(0) per visualizzare immediatamente la barra di avanzamento. Turbo.session.drive = false disabilita temporaneamente Turbo Drive.

Gestione degli Errori e Stati di Caricamento

Una buona UX richiede la gestione degli stati di caricamento e degli errori di rete.

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

Gestione degli Errori del Server

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

Ottimizzazione delle Prestazioni

Diverse tecniche assicurano applicazioni Turbo performanti.

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

Conclusione

Hotwire e Turbo trasformano lo sviluppo Rails eliminando la complessita dei framework JavaScript tradizionali. Turbo Drive accelera la navigazione, Turbo Frames consentono aggiornamenti mirati, e Turbo Streams offre potenti capacita in tempo reale.

Checklist per Iniziare con Hotwire

  • Utilizzare Rails 7+ per l'integrazione Hotwire integrata
  • Comprendere i tre componenti: Drive, Frames e Streams
  • Identificare le zone della pagina che beneficiano dei Turbo Frames
  • Utilizzare respond_to con format.turbo_stream nei controller
  • Implementare il broadcasting per le funzionalita in tempo reale
  • Combinare con Stimulus per interazioni JavaScript semplici

Inizia a praticare!

Metti alla prova le tue conoscenze con i nostri simulatori di colloquio e test tecnici.

Questo approccio "HTML via cavo" consente di costruire applicazioni moderne e reattive mantenendo la semplicita e la produttivita che rendono Ruby on Rails cosi potente. Il risultato: meno codice da mantenere, prestazioni eccellenti e un'esperienza di sviluppo ottimale.

Tag

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

Condividi