Ruby on Rails 7: Hotwire dan Turbo untuk Aplikasi Reaktif
Panduan lengkap Hotwire dan Turbo di Rails 7. Membangun aplikasi reaktif tanpa JavaScript dengan Turbo Drive, Frames dan Streams.

Rails 7 merevolusi pengembangan web dengan mengintegrasikan Hotwire secara default. Stack ini memungkinkan pembangunan aplikasi yang sangat reaktif tanpa menulis satu baris pun kode JavaScript kustom. Turbo Drive, Turbo Frames, dan Turbo Streams menggantikan pendekatan SPA tradisional dengan filosofi "HTML melalui kabel".
Hotwire secara drastis mengurangi kompleksitas frontend. Tidak perlu React atau Vue untuk antarmuka dinamis: server mengirimkan HTML siap pakai, dan Turbo menangani pembaruan DOM secara otomatis.
Memahami Arsitektur Hotwire
Hotwire terdiri dari tiga teknologi komplementer yang bekerja sama untuk memberikan pengalaman pengguna yang mulus tanpa kompleksitas tipikal framework JavaScript.
Turbo Drive mempercepat navigasi dengan mengintersep klik tautan dan pengiriman formulir. Alih-alih memuat ulang seluruh halaman, hanya konten <body> yang diganti, menjaga konteks JavaScript dan CSS.
Turbo Frames memecah halaman menjadi bagian-bagian independen. Setiap frame dapat diperbarui secara terpisah, memungkinkan interaksi tertarget tanpa mempengaruhi bagian halaman lainnya.
Turbo Streams memungkinkan pembaruan real-time melalui WebSocket atau sebagai respons terhadap permintaan HTTP. Delapan aksi CRUD tersedia untuk manipulasi DOM deklaratif.
# Gemfile
# Installing Turbo Rails (included by default in Rails 7+)
gem 'turbo-rails'Untuk proyek yang sudah ada, instalasi hanya membutuhkan satu perintah.
# 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"Konfigurasi Awal Turbo Drive
Turbo Drive diaktifkan secara default di Rails 7. Semua navigasi secara otomatis menjadi "turbo" tanpa konfigurasi tambahan. Perilaku dapat disesuaikan melalui atribut 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 dapat dinonaktifkan pada tautan atau formulir tertentu jika diperlukan.
<!-- 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 meng-cache halaman yang dikunjungi. Atribut data-turbo-track="reload" pada aset memaksa pemuatan ulang penuh jika file CSS/JS berubah.
Turbo Frames untuk Pembaruan Tertarget
Turbo Frames mendefinisikan zona halaman yang diperbarui secara independen. Setiap frame memiliki identifier unik dan hanya merespons respons yang mengandung frame yang cocok.
<!-- 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>Respons server harus mengandung frame dengan identifier yang sama agar pembaruan berfungsi.
<!-- 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>Lazy Loading dengan Turbo Frames
Frame dapat memuat kontennya secara asinkron menggunakan atribut src. Konten awal ditampilkan selama pemuatan.
<!-- 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>Controller merespons dengan view yang hanya berisi frame yang diminta.
# 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>Siap menguasai wawancara Ruby on Rails Anda?
Berlatih dengan simulator interaktif, flashcards, dan tes teknis kami.
Turbo Streams untuk Pembaruan Real-Time
Turbo Streams menawarkan delapan aksi untuk manipulasi DOM: append, prepend, replace, update, remove, before, after, dan morph. Aksi-aksi ini dapat dipicu melalui respons HTTP atau 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 Turbo Stream Khusus
Untuk respons yang lebih kompleks, template .turbo_stream.erb menawarkan fleksibilitas lebih.
<!-- 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 %>Respons Turbo Stream harus memiliki content-type text/vnd.turbo-stream.html. Rails menangani ini secara otomatis dengan respond_to dan format turbo_stream.
Broadcasting Real-Time dengan Action Cable
Turbo Streams benar-benar bersinar dengan broadcasting WebSocket. Pengguna menerima pembaruan secara instan tanpa 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
endView berlangganan ke stream yang sesuai dengan 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 Asinkron untuk Operasi Berat
Untuk menghindari pemblokiran permintaan, broadcasting dapat dilakukan di latar belakang.
# 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
endPola Lanjutan dengan Turbo
Pengeditan Formulir Inline
Pola umum melibatkan penggantian konten statis dengan formulir edit 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>Navigasi di Luar Frame Induk
Secara default, tautan dalam frame tetap dalam frame tersebut. Atribut data-turbo-frame memungkinkan penargetan frame lain atau seluruh halaman.
<!-- 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>Di lingkungan development, buka konsol JavaScript dan ketik Turbo.setProgressBarDelay(0) untuk melihat progress bar secara langsung. Turbo.session.drive = false menonaktifkan Turbo Drive sementara.
Penanganan Error dan Status Loading
UX yang baik memerlukan penanganan status loading dan error jaringan.
<!-- 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")
}
}Penanganan Error 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
endOptimasi Performa
Beberapa teknik memastikan aplikasi Turbo yang performan.
# 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 %>Kesimpulan
Hotwire dan Turbo mentransformasi pengembangan Rails dengan menghilangkan kompleksitas framework JavaScript tradisional. Turbo Drive mempercepat navigasi, Turbo Frames memungkinkan pembaruan tertarget, dan Turbo Streams menawarkan kemampuan real-time yang powerful.
Checklist untuk Memulai dengan Hotwire
- Gunakan Rails 7+ untuk integrasi Hotwire bawaan
- Pahami tiga komponen: Drive, Frames, dan Streams
- Identifikasi zona halaman yang diuntungkan dari Turbo Frames
- Gunakan
respond_todenganformat.turbo_streamdi controller - Implementasikan broadcasting untuk fitur real-time
- Kombinasikan dengan Stimulus untuk interaksi JavaScript sederhana
Mulai berlatih!
Uji pengetahuan Anda dengan simulator wawancara dan tes teknis kami.
Pendekatan "HTML melalui kabel" ini memungkinkan pembangunan aplikasi modern dan reaktif sambil mempertahankan kesederhanaan dan produktivitas yang membuat Ruby on Rails begitu powerful. Hasilnya: lebih sedikit kode yang perlu dipelihara, performa yang sangat baik, dan pengalaman developer yang optimal.
Tag
Bagikan