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.

Panduan Hotwire dan Turbo untuk Ruby on Rails 7

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

Mengapa Hotwire?

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.

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

Untuk proyek yang sudah ada, instalasi hanya membutuhkan satu perintah.

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"

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.

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 dapat dinonaktifkan pada tautan atau formulir tertentu jika diperlukan.

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

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.

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>

Respons server harus mengandung frame dengan identifier yang sama agar pembaruan berfungsi.

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>

Lazy Loading dengan Turbo Frames

Frame dapat memuat kontennya secara asinkron menggunakan atribut src. Konten awal ditampilkan selama pemuatan.

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>

Controller merespons dengan view yang hanya berisi frame yang diminta.

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>

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.

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

Template Turbo Stream Khusus

Untuk respons yang lebih kompleks, template .turbo_stream.erb menawarkan fleksibilitas lebih.

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

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.

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

View berlangganan ke stream yang sesuai dengan 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 Asinkron untuk Operasi Berat

Untuk menghindari pemblokiran permintaan, broadcasting dapat dilakukan di latar belakang.

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

Pola Lanjutan dengan Turbo

Pengeditan Formulir Inline

Pola umum melibatkan penggantian konten statis dengan formulir edit inline.

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>

Secara default, tautan dalam frame tetap dalam frame tersebut. Atribut data-turbo-frame memungkinkan penargetan frame lain atau seluruh halaman.

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>
Debugging Turbo

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.

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

Penanganan Error Server

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

Optimasi Performa

Beberapa teknik memastikan aplikasi Turbo yang performan.

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

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_to dengan format.turbo_stream di 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

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

Bagikan