Ruby on Rails 7: Hotwire y Turbo para Aplicaciones Reactivas
Guia completa de Hotwire y Turbo en Rails 7. Aprende a construir aplicaciones reactivas sin escribir JavaScript con Turbo Drive, Frames y Streams.

Rails 7 revoluciono el desarrollo web al integrar Hotwire por defecto. Este stack permite construir aplicaciones altamente reactivas sin escribir una sola linea de JavaScript personalizado. Turbo Drive, Turbo Frames y Turbo Streams reemplazan los enfoques SPA tradicionales con una filosofia de "HTML sobre el cable".
Hotwire reduce drasticamente la complejidad del frontend. No se necesita React ni Vue para interfaces dinamicas: el servidor envia HTML listo para usar, y Turbo se encarga de las actualizaciones del DOM automaticamente.
Comprendiendo la Arquitectura de Hotwire
Hotwire consiste en tres tecnologias complementarias que trabajan juntas para ofrecer una experiencia de usuario fluida sin la complejidad tipica de los frameworks JavaScript.
Turbo Drive acelera la navegacion interceptando clics en enlaces y envios de formularios. En lugar de recargar toda la pagina, solo se reemplaza el contenido del <body>, preservando el contexto de JavaScript y CSS.
Turbo Frames descomponen las paginas en secciones independientes. Cada frame puede actualizarse por separado, permitiendo interacciones dirigidas sin afectar el resto de la pagina.
Turbo Streams permiten actualizaciones en tiempo real via WebSocket o en respuesta a peticiones HTTP. Ocho acciones CRUD estan disponibles para la manipulacion declarativa del DOM.
# Gemfile
# Installing Turbo Rails (included by default in Rails 7+)
gem 'turbo-rails'Para proyectos existentes, la instalacion requiere solo un 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"Configuracion Inicial de Turbo Drive
Turbo Drive esta habilitado por defecto en Rails 7. Toda la navegacion se convierte automaticamente en "turbo" sin configuracion adicional. El comportamiento puede personalizarse mediante atributos 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 puede desactivarse en enlaces o formularios especificos cuando sea necesario.
<!-- 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 almacena en cache las paginas visitadas. El atributo data-turbo-track="reload" en los assets fuerza una recarga completa si los archivos CSS/JS cambian.
Turbo Frames para Actualizaciones Dirigidas
Turbo Frames definen zonas de la pagina que se actualizan de forma independiente. Cada frame tiene un identificador unico y solo responde a respuestas que contengan un frame coincidente.
<!-- 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 respuesta del servidor debe contener un frame con el mismo identificador para que la actualizacion funcione.
<!-- 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>Carga Diferida con Turbo Frames
Los frames pueden cargar su contenido de forma asincrona usando el atributo src. El contenido inicial se muestra durante la carga.
<!-- 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>El controlador responde con una vista que contiene unicamente el frame solicitado.
# 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>¿Listo para aprobar tus entrevistas de Ruby on Rails?
Practica con nuestros simuladores interactivos, flashcards y tests técnicos.
Turbo Streams para Actualizaciones en Tiempo Real
Turbo Streams ofrece ocho acciones para la manipulacion del DOM: append, prepend, replace, update, remove, before, after y morph. Estas acciones pueden activarse mediante respuesta 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
endPlantillas Dedicadas de Turbo Stream
Para respuestas mas complejas, una plantilla .turbo_stream.erb ofrece mayor flexibilidad.
<!-- 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 %>Las respuestas de Turbo Stream deben tener el content-type text/vnd.turbo-stream.html. Rails maneja esto automaticamente con respond_to y el formato turbo_stream.
Broadcasting en Tiempo Real con Action Cable
Turbo Streams realmente brilla con el broadcasting por WebSocket. Los usuarios reciben actualizaciones al instante sin 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 vista se suscribe al stream correspondiente con el 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 para Operaciones Pesadas
Para evitar bloquear las peticiones, el broadcasting puede realizarse en segundo plano.
# 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
endPatrones Avanzados con Turbo
Edicion de Formularios en Linea
Un patron comun consiste en reemplazar contenido estatico con un formulario de edicion en linea.
<!-- 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>Navegacion Fuera del Frame Padre
Por defecto, los enlaces en un frame permanecen dentro de ese frame. El atributo data-turbo-frame permite apuntar a otro frame o a la pagina completa.
<!-- 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>En desarrollo, se puede abrir la consola JavaScript y escribir Turbo.setProgressBarDelay(0) para ver la barra de progreso inmediatamente. Turbo.session.drive = false desactiva Turbo Drive temporalmente.
Manejo de Errores y Estados de Carga
Una buena UX requiere manejar estados de carga y errores de red.
<!-- 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")
}
}Manejo de Errores del Servidor
# 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
endOptimizacion del Rendimiento
Varias tecnicas aseguran aplicaciones Turbo con buen rendimiento.
# 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 %>Conclusion
Hotwire y Turbo transforman el desarrollo en Rails al eliminar la complejidad de los frameworks JavaScript tradicionales. Turbo Drive acelera la navegacion, Turbo Frames permiten actualizaciones dirigidas, y Turbo Streams ofrece potentes capacidades en tiempo real.
Lista de Verificacion para Comenzar con Hotwire
- Usar Rails 7+ para la integracion integrada de Hotwire
- Comprender los tres componentes: Drive, Frames y Streams
- Identificar zonas de la pagina que se benefician de Turbo Frames
- Usar
respond_toconformat.turbo_streamen los controladores - Implementar broadcasting para funcionalidades en tiempo real
- Combinar con Stimulus para interacciones JavaScript simples
¡Empieza a practicar!
Pon a prueba tu conocimiento con nuestros simuladores de entrevista y tests técnicos.
Este enfoque de "HTML sobre el cable" permite construir aplicaciones modernas y reactivas manteniendo la simplicidad y productividad que hacen a Ruby on Rails tan poderoso. El resultado: menos codigo que mantener, excelente rendimiento y una experiencia de desarrollo optima.
Etiquetas
Compartir