Ruby on Rails 7 : Hotwire et Turbo pour des applications réactives
Guide complet sur Hotwire et Turbo dans Rails 7. Apprenez à créer des applications réactives sans écrire de JavaScript avec Turbo Drive, Frames et Streams.

Rails 7 a révolutionné le développement web en intégrant Hotwire par défaut. Cette stack permet de créer des applications hautement réactives sans écrire une ligne de JavaScript custom. Turbo Drive, Turbo Frames et Turbo Streams remplacent les approches SPA traditionnelles par une philosophie "HTML over the wire".
Hotwire réduit drastiquement la complexité frontend. Plus besoin de React ou Vue pour des interfaces dynamiques : le serveur envoie du HTML prêt à l'emploi, et Turbo gère les mises à jour du DOM automatiquement.
Comprendre l'architecture Hotwire
Hotwire se compose de trois technologies complémentaires qui travaillent ensemble pour offrir une expérience utilisateur fluide sans la complexité habituelle des frameworks JavaScript.
Turbo Drive accélère la navigation en interceptant les clics sur les liens et les soumissions de formulaires. Au lieu de recharger toute la page, seul le contenu du <body> est remplacé, préservant le contexte JavaScript et CSS.
Turbo Frames décomposent les pages en sections indépendantes. Chaque frame peut être mise à jour séparément, permettant des interactions ciblées sans affecter le reste de la page.
Turbo Streams permettent des mises à jour en temps réel via WebSocket ou en réponse aux requêtes HTTP. Huit actions CRUD sont disponibles pour manipuler le DOM de manière déclarative.
# Gemfile
# Installation de Turbo Rails (inclus par défaut dans Rails 7+)
gem 'turbo-rails'Pour les projets existants, l'installation se fait en une commande.
# installation.sh
# Installation de Turbo dans un projet Rails existant
rails turbo:install
# Vérifie que l'import JavaScript est présent
cat app/javascript/application.js
# Devrait contenir : import "@hotwired/turbo-rails"Configuration initiale de Turbo Drive
Turbo Drive est activé par défaut dans Rails 7. Toute navigation devient automatiquement "turbo" sans configuration supplémentaire. Le comportement peut être personnalisé via des attributs 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>
<!-- Barre de progression pendant les navigations -->
<div class="turbo-progress-bar"></div>
<%= yield %>
</body>
</html>Turbo Drive peut être désactivé sur des liens ou formulaires spécifiques quand nécessaire.
<!-- app/views/pages/examples.html.erb -->
<!-- Lien avec Turbo activé (par défaut) -->
<%= link_to "Dashboard", dashboard_path %>
<!-- Désactiver Turbo pour un lien spécifique -->
<%= link_to "Télécharger PDF", export_path, data: { turbo: false } %>
<!-- Désactiver Turbo pour un formulaire (upload de fichier par exemple) -->
<%= form_with model: @document, data: { turbo: false } do |f| %>
<%= f.file_field :attachment %>
<%= f.submit "Uploader" %>
<% end %>
<!-- Turbo avec confirmation -->
<%= link_to "Supprimer", item_path(@item),
data: { turbo_method: :delete, turbo_confirm: "Êtes-vous sûr ?" } %>Turbo Drive met en cache les pages visitées. L'attribut data-turbo-track="reload" sur les assets force un rechargement complet si les fichiers CSS/JS changent.
Turbo Frames pour les mises à jour ciblées
Les Turbo Frames permettent de définir des zones de la page qui se mettent à jour indépendamment. Chaque frame possède un identifiant unique et ne réagit qu'aux réponses contenant un frame correspondant.
<!-- app/views/messages/index.html.erb -->
<h1>Messages</h1>
<!-- Frame pour la liste des messages -->
<turbo-frame id="messages_list">
<div id="messages">
<%= render @messages %>
</div>
<%= link_to "Charger plus", messages_path(page: @next_page) %>
</turbo-frame>
<!-- Frame pour le formulaire de création -->
<turbo-frame id="new_message">
<%= render "form", message: Message.new %>
</turbo-frame>La réponse du serveur doit contenir un frame avec le même identifiant pour que la mise à jour fonctionne.
<!-- app/views/messages/_message.html.erb -->
<!-- Partial pour un message individuel -->
<turbo-frame id="<%= dom_id(message) %>">
<div class="message">
<p><%= message.content %></p>
<span class="metadata">
Par <%= message.author.name %> - <%= time_ago_in_words(message.created_at) %>
</span>
<!-- Ces liens restent dans le frame -->
<%= link_to "Éditer", edit_message_path(message) %>
<%= button_to "Supprimer", message_path(message), method: :delete %>
</div>
</turbo-frame>Chargement différé avec Turbo Frames
Les frames peuvent charger leur contenu de manière asynchrone grâce à l'attribut src. Le contenu initial est affiché pendant le chargement.
<!-- app/views/dashboard/show.html.erb -->
<h1>Tableau de bord</h1>
<!-- Statistiques chargées de manière asynchrone -->
<turbo-frame id="stats" src="<%= dashboard_stats_path %>">
<div class="loading-placeholder">
<p>Chargement des statistiques...</p>
</div>
</turbo-frame>
<!-- Notifications chargées quand visibles (lazy loading) -->
<turbo-frame id="notifications"
src="<%= notifications_path %>"
loading="lazy">
<p>Chargement des notifications...</p>
</turbo-frame>
<!-- Activité récente avec actualisation périodique -->
<turbo-frame id="recent_activity"
src="<%= activity_path %>"
data-controller="auto-refresh"
data-auto-refresh-interval-value="30000">
<%= render "activity/placeholder" %>
</turbo-frame>Le contrôleur répond avec une vue contenant uniquement le frame demandé.
# 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
# Rendu partiel pour le 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>Utilisateurs</p>
</div>
<div class="stat-card">
<h3><%= @message_count %></h3>
<p>Messages</p>
</div>
<div class="stat-card">
<h3><%= @active_today %></h3>
<p>Actifs aujourd'hui</p>
</div>
</div>
</turbo-frame>Prêt à réussir tes entretiens Ruby on Rails ?
Entraîne-toi avec nos simulateurs interactifs, fiches express et tests techniques.
Turbo Streams pour les mises à jour en temps réel
Turbo Streams offrent huit actions pour manipuler le DOM : append, prepend, replace, update, remove, before, after et morph. Ces actions peuvent être déclenchées en réponse HTTP ou via 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
# Réponse Turbo Stream pour ajouter le 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 créé" }
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|
# Suppression avec animation
format.turbo_stream { render turbo_stream: turbo_stream.remove(@message) }
format.html { redirect_to messages_path, notice: "Message supprimé" }
end
end
endTemplates Turbo Stream dédiés
Pour des réponses plus complexes, un template .turbo_stream.erb offre plus de flexibilité.
<!-- app/views/messages/create.turbo_stream.erb -->
<!-- Ajoute le nouveau message en haut de la liste -->
<%= turbo_stream.prepend "messages" do %>
<%= render @message %>
<% end %>
<!-- Met à jour le compteur -->
<%= turbo_stream.update "message_count" do %>
<span><%= Message.count %> messages</span>
<% end %>
<!-- Réinitialise le formulaire -->
<%= turbo_stream.replace "new_message" do %>
<%= render "form", message: Message.new %>
<% end %>
<!-- Affiche une notification flash -->
<%= turbo_stream.prepend "flash_messages" do %>
<div class="flash flash-success" data-controller="auto-dismiss">
Message envoyé avec succès !
</div>
<% end %>Les réponses Turbo Stream doivent avoir le content-type text/vnd.turbo-stream.html. Rails le gère automatiquement avec respond_to et le format turbo_stream.
Broadcasting en temps réel avec Action Cable
Turbo Streams brillent vraiment avec le broadcasting WebSocket. Les utilisateurs reçoivent les mises à jour instantanément sans polling.
# app/models/message.rb
class Message < ApplicationRecord
belongs_to :room
belongs_to :author, class_name: "User"
# Broadcast automatique après création
after_create_commit do
broadcast_append_to(
room,
target: "messages",
partial: "messages/message",
locals: { message: self }
)
end
# Broadcast après mise à jour
after_update_commit do
broadcast_replace_to(
room,
target: dom_id(self),
partial: "messages/message",
locals: { message: self }
)
end
# Broadcast après suppression
after_destroy_commit do
broadcast_remove_to(room, target: dom_id(self))
end
endLa vue s'abonne au stream correspondant avec le helper turbo_stream_from.
<!-- app/views/rooms/show.html.erb -->
<h1><%= @room.name %></h1>
<!-- Abonnement au stream WebSocket -->
<%= turbo_stream_from @room %>
<!-- Container pour les messages -->
<div id="messages">
<%= render @room.messages.order(created_at: :asc) %>
</div>
<!-- Formulaire de nouveau message -->
<%= form_with model: [@room, Message.new], id: "new_message" do |f| %>
<%= f.text_area :content, placeholder: "Votre message..." %>
<%= f.submit "Envoyer" %>
<% end %>Broadcast asynchrone pour les opérations lourdes
Pour éviter de bloquer les requêtes, le broadcast peut être effectué en arrière-plan.
# 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)
# Simulation d'une opération longue
report.update!(status: "processing")
# Notifie l'utilisateur du début
report.broadcast_replace_to(
report.user, :reports,
target: dom_id(report),
partial: "reports/report"
)
# Génération du rapport
result = ReportGenerator.new(report).generate
report.update!(content: result, status: "completed")
# Notifie l'utilisateur de la fin
report.broadcast_replace_to(
report.user, :reports,
target: dom_id(report),
partial: "reports/report"
)
end
endPatterns avancés avec Turbo
Formulaire inline avec édition
Un pattern courant consiste à remplacer du contenu statique par un formulaire d'édition 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 "Éditer", edit_task_path(task) %>
<%= button_to "Supprimer", task_path(task), method: :delete,
data: { turbo_confirm: "Supprimer cette tâche ?" } %>
</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 "Sauvegarder" %>
<%= link_to "Annuler", task_path(@task) %>
</div>
<% end %>
</turbo-frame>Navigation hors du frame parent
Par défaut, les liens dans un frame restent dans ce frame. L'attribut data-turbo-frame permet de cibler un autre frame ou la page entière.
<!-- app/views/search/_results.html.erb -->
<turbo-frame id="search_results">
<ul>
<% @results.each do |result| %>
<li>
<!-- Ce lien navigue dans la page entière, pas dans le frame -->
<%= link_to result.title, result_path(result),
data: { turbo_frame: "_top" } %>
</li>
<% end %>
</ul>
</turbo-frame><!-- app/views/layouts/_sidebar.html.erb -->
<aside>
<!-- Ce frame charge son contenu et met à jour le main -->
<turbo-frame id="sidebar_nav" target="main_content">
<%= render "navigation" %>
</turbo-frame>
</aside>
<main>
<turbo-frame id="main_content">
<%= yield %>
</turbo-frame>
</main>En développement, ouvrez la console JavaScript et tapez Turbo.setProgressBarDelay(0) pour voir immédiatement la barre de progression. Turbo.session.drive = false désactive Turbo Drive temporairement.
Gestion des erreurs et états de chargement
Une bonne UX nécessite de gérer les états de chargement et les erreurs réseau.
<!-- 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">Sauvegarder</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")
}
}Gestion des erreurs serveur
# 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
endOptimisation des performances
Quelques techniques pour des applications Turbo performantes.
# app/controllers/messages_controller.rb
class MessagesController < ApplicationController
# Évite les requêtes N+1 avec eager loading
def index
@messages = Message.includes(:author, :room)
.order(created_at: :desc)
.page(params[:page])
end
# Cache fragment pour les éléments statiques
def show
@message = Message.find(params[:id])
fresh_when @message
end
end<!-- app/views/messages/_message.html.erb -->
<!-- Cache au niveau du partial -->
<% cache message do %>
<turbo-frame id="<%= dom_id(message) %>">
<div class="message">
<%= message.content %>
<small>Par <%= message.author.name %></small>
</div>
</turbo-frame>
<% end %>Conclusion
Hotwire et Turbo transforment le développement Rails en éliminant la complexité des frameworks JavaScript traditionnels. Turbo Drive accélère la navigation, Turbo Frames permettent des mises à jour ciblées, et Turbo Streams offrent des capacités temps réel puissantes.
Checklist pour bien démarrer avec Hotwire
- ✅ Utiliser Rails 7+ pour avoir Hotwire intégré par défaut
- ✅ Comprendre les trois composants : Drive, Frames et Streams
- ✅ Identifier les zones de la page qui bénéficient de Turbo Frames
- ✅ Utiliser
respond_toavecformat.turbo_streamdans les contrôleurs - ✅ Implémenter le broadcasting pour les fonctionnalités temps réel
- ✅ Combiner avec Stimulus pour les interactions JavaScript simples
Passe à la pratique !
Teste tes connaissances avec nos simulateurs d'entretien et tests techniques.
Cette approche "HTML over the wire" permet de construire des applications modernes et réactives tout en gardant la simplicité et la productivité qui font la force de Ruby on Rails. Le résultat : moins de code à maintenir, des performances excellentes, et une expérience développeur optimale.
Tags
Partager
Articles similaires

ActiveRecord : Optimiser les requêtes N+1 dans Ruby on Rails
Guide complet pour détecter et corriger les problèmes de requêtes N+1 dans Rails avec ActiveRecord. Includes, preload, eager_load et outils de détection.

Questions d'entretien Ruby on Rails : Top 25 en 2026
Les 25 questions d'entretien Ruby on Rails les plus posées. Architecture MVC, Active Record, migrations, tests RSpec, API REST avec réponses détaillées et exemples de code.

Action Cable et WebSockets dans Rails : Guide Complet pour les Entretiens Techniques
Action Cable integre les WebSockets directement dans Rails. Ce guide approfondi couvre l'architecture des connexions, les channels, Solid Cable, Turbo Streams, le scaling avec Redis et les patterns de test pour les entretiens techniques.