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.

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".
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.
# Gemfile
# Installing Turbo Rails (included by default in Rails 7+)
gem 'turbo-rails'Per i progetti esistenti, l'installazione richiede un solo comando.
# 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.
<!-- 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.
<!-- 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 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.
<!-- 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.
<!-- 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.
<!-- 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.
# 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>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.
# 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
endTemplate Dedicati per Turbo Stream
Per risposte piu complesse, un template .turbo_stream.erb offre maggiore flessibilita.
<!-- 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 %>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.
# 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
endLa view si iscrive allo stream corrispondente con l'helper turbo_stream_from.
<!-- 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.
# 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
endPattern Avanzati con Turbo
Modifica Inline dei Form
Un pattern comune consiste nel sostituire il contenuto statico con un form di modifica inline.
<!-- 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>Navigazione Fuori dal Frame Padre
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.
<!-- 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 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.
<!-- 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")
}
}Gestione degli Errori del Server
# 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
endOttimizzazione delle Prestazioni
Diverse tecniche assicurano applicazioni Turbo performanti.
# 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 %>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_toconformat.turbo_streamnei 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
Condividi