Ruby on Rails 7: Hotwire e Turbo para Aplicacoes Reativas
Guia completo de Hotwire e Turbo no Rails 7. Aprenda a construir aplicacoes reativas sem escrever JavaScript com Turbo Drive, Frames e Streams.

O Rails 7 revolucionou o desenvolvimento web ao integrar o Hotwire por padrao. Essa stack permite construir aplicacoes altamente reativas sem escrever uma unica linha de JavaScript personalizado. Turbo Drive, Turbo Frames e Turbo Streams substituem as abordagens tradicionais de SPA com uma filosofia de "HTML pelo fio".
O Hotwire reduz drasticamente a complexidade do frontend. Nao e necessario React ou Vue para interfaces dinamicas: o servidor envia HTML pronto para uso, e o Turbo cuida das atualizacoes do DOM automaticamente.
Entendendo a Arquitetura do Hotwire
O Hotwire consiste em tres tecnologias complementares que trabalham juntas para entregar uma experiencia de usuario fluida sem a complexidade tipica dos frameworks JavaScript.
Turbo Drive acelera a navegacao interceptando cliques em links e envios de formularios. Em vez de recarregar a pagina inteira, apenas o conteudo do <body> e substituido, preservando o contexto de JavaScript e CSS.
Turbo Frames decompoe as paginas em secoes independentes. Cada frame pode ser atualizado separadamente, permitindo interacoes direcionadas sem afetar o restante da pagina.
Turbo Streams permitem atualizacoes em tempo real via WebSocket ou em resposta a requisicoes HTTP. Oito acoes CRUD estao disponiveis para manipulacao declarativa do DOM.
# Gemfile
# Installing Turbo Rails (included by default in Rails 7+)
gem 'turbo-rails'Para projetos existentes, a instalacao requer apenas um 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"Configuracao Inicial do Turbo Drive
O Turbo Drive vem habilitado por padrao no Rails 7. Toda a navegacao automaticamente se torna "turbo" sem configuracao adicional. O comportamento pode ser personalizado por meio de 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>O Turbo Drive pode ser desativado em links ou formularios especificos 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?" } %>O Turbo Drive armazena em cache as paginas visitadas. O atributo data-turbo-track="reload" nos assets forca um recarregamento completo se os arquivos CSS/JS mudarem.
Turbo Frames para Atualizacoes Direcionadas
Turbo Frames definem zonas da pagina que se atualizam de forma independente. Cada frame possui um identificador unico e so responde a respostas que contenham um frame correspondente.
<!-- 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>A resposta do servidor deve conter um frame com o mesmo identificador para que a atualizacao 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>Carregamento Lazy com Turbo Frames
Os frames podem carregar seu conteudo de forma assincrona usando o atributo src. O conteudo inicial e exibido durante o carregamento.
<!-- 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>O controller responde com uma view contendo apenas o 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>Pronto para mandar bem nas entrevistas de Ruby on Rails?
Pratique com nossos simuladores interativos, flashcards e testes tecnicos.
Turbo Streams para Atualizacoes em Tempo Real
Turbo Streams oferece oito acoes para manipulacao do DOM: append, prepend, replace, update, remove, before, after e morph. Essas acoes podem ser acionadas via resposta HTTP ou 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
endTemplates Dedicados de Turbo Stream
Para respostas mais complexas, um template .turbo_stream.erb oferece maior flexibilidade.
<!-- 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 %>As respostas de Turbo Stream devem ter o content-type text/vnd.turbo-stream.html. O Rails trata isso automaticamente com respond_to e o formato turbo_stream.
Broadcasting em Tempo Real com Action Cable
Turbo Streams realmente brilha com o broadcasting via WebSocket. Os usuarios recebem atualizacoes instantaneamente sem 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
endA view se inscreve no stream correspondente com o 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 Assincrono para Operacoes Pesadas
Para evitar bloqueio de requisicoes, o broadcasting pode ser realizado em 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
endPadroes Avancados com Turbo
Edicao Inline de Formularios
Um padrao comum envolve substituir conteudo estatico por um formulario de edicao 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>Navegacao Fora do Frame Pai
Por padrao, os links em um frame permanecem dentro desse frame. O atributo data-turbo-frame permite apontar para outro frame ou para a pagina inteira.
<!-- 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>Em desenvolvimento, abra o console JavaScript e digite Turbo.setProgressBarDelay(0) para ver a barra de progresso imediatamente. Turbo.session.drive = false desativa o Turbo Drive temporariamente.
Tratamento de Erros e Estados de Carregamento
Uma boa UX exige o tratamento de estados de carregamento e erros de rede.
<!-- 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")
}
}Tratamento de Erros do 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
endOtimizacao de Performance
Varias tecnicas garantem aplicacoes Turbo com bom desempenho.
# 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 %>Conclusao
Hotwire e Turbo transformam o desenvolvimento em Rails ao eliminar a complexidade dos frameworks JavaScript tradicionais. Turbo Drive acelera a navegacao, Turbo Frames permitem atualizacoes direcionadas, e Turbo Streams oferece poderosas capacidades em tempo real.
Checklist para Comecar com Hotwire
- Usar Rails 7+ para integracao nativa com Hotwire
- Compreender os tres componentes: Drive, Frames e Streams
- Identificar zonas da pagina que se beneficiam de Turbo Frames
- Usar
respond_tocomformat.turbo_streamnos controllers - Implementar broadcasting para funcionalidades em tempo real
- Combinar com Stimulus para interacoes JavaScript simples
Comece a praticar!
Teste seus conhecimentos com nossos simuladores de entrevista e testes tecnicos.
Essa abordagem de "HTML pelo fio" permite construir aplicacoes modernas e reativas mantendo a simplicidade e produtividade que tornam o Ruby on Rails tao poderoso. O resultado: menos codigo para manter, excelente desempenho e uma experiencia de desenvolvimento otima.
Tags
Compartilhar