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.

Guia de Hotwire y Turbo para Ruby on Rails 7

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

Por que Hotwire?

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.

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

Para proyectos existentes, la instalacion requiere solo un 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"

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.

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 puede desactivarse en enlaces o formularios especificos cuando sea necesario.

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 de Turbo Drive

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.

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 respuesta del servidor debe contener un frame con el mismo identificador para que la actualizacion funcione.

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>

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.

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>

El controlador responde con una vista que contiene unicamente el frame solicitado.

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>

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

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

Plantillas Dedicadas de Turbo Stream

Para respuestas mas complejas, una plantilla .turbo_stream.erb ofrece mayor flexibilidad.

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 de Respuesta

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.

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 vista se suscribe al stream correspondiente con el 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 para Operaciones Pesadas

Para evitar bloquear las peticiones, el broadcasting puede realizarse en segundo plano.

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

Patrones Avanzados con Turbo

Edicion de Formularios en Linea

Un patron comun consiste en reemplazar contenido estatico con un formulario de edicion en linea.

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>

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.

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>
Depuracion de Turbo

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.

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

Manejo de Errores del Servidor

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

Optimizacion del Rendimiento

Varias tecnicas aseguran aplicaciones Turbo con buen rendimiento.

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

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_to con format.turbo_stream en 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

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

Compartir