Membangun Realtime Dashboard dengan Phoenix LiveView: UI Server-Driven tanpa JavaScript
Mengenal Konsep Server-Driven UI dan Peran LiveView
Arsitektur SPA klasik menempatkan logika state di dua tempat sekaligus: di browser untuk rendering dan di server untuk data. Dua sumber kebenaran ini memaksa kita menulis validasi dua kali, menjaga kontrak API tetap sinkron, dan mengirim bundle JavaScript yang semakin besar. Semakin kompleks dashboard, semakin banyak kode yang harus kita rawat di sisi client.
Phoenix LiveView menawarkan pendekatan yang berbeda. State dan rendering dipindahkan sepenuhnya ke server, sementara browser hanya menjadi thin client yang menampilkan hasil render. Koneksi WebSocket membawa potongan HTML yang sudah dirender, bukan payload data mentah yang harus diolah JavaScript. Server mengirim hanya fragmen HTML yang berubah, mirip cara version control mengirim diff alih-alih seluruh file.
Konsekuensinya langsung terasa. Kita tidak lagi membutuhkan library state management seperti Redux atau MobX, karena tidak ada state client yang harus disinkronkan. Logika bisnis, validasi, dan rendering berada dalam satu bahasa dan satu proses. Kondisi ini membuat dashboard lebih mudah dipahami dan diuji.

Gambar: Diagram alur koneksi WebSocket yang memungkinkan komunikasi dua arah persisten antara browser dan server — Sumber: Wikimedia Commons
Menyiapkan Proyek Phoenix dengan LiveView
Langkah pertama adalah memastikan Elixir dan Phoenix terpasang. Kita install archive phx_new, lalu generate proyek baru dengan flag --live. Flag ini otomatis menambahkan phoenix_live_view, phoenix_live_reload, dan floki ke dalam dependencies.
mix archive.install hex phx_new
mix phx.new dashboard --live
cd dashboard
mix deps.get
mix phx.serverSetelah proyek terbentuk, kita perlu memastikan endpoint LiveView aktif. Konfigurasi signing_salt di config/config.exs dan route live di router menjadi dua bagian yang wajib ada.
# config/config.exs
config :dashboard, DashboardWeb.Endpoint,
live_view: [signing_salt: "CHANGE_ME"]
# lib/dashboard_web/router.ex
scope "/", DashboardWeb do
pipe_through :browser
live "/", DashboardLive.Index, :index
endSaat mix phx.server berjalan, browser membuka halaman default dan menghubungkan socket LiveView. Kita bisa memverifikasi koneksi ini lewat console browser, di mana pesan seperti LiveView connection established muncul. Setelah koneksi terbentuk, setiap perubahan di server akan langsung dirender ulang tanpa reload.
Membangun LiveView Module Pertama dengan State di Server
Setiap LiveView mengikuti siklus hidup yang jelas. mount/3 menginisialisasi state, render/1 menghasilkan HTML dari state tersebut, dan handle_event/3 merespons aksi pengguna. State disimpan di socket.assigns, yang seluruhnya berada di server.
defmodule DashboardWeb.DashboardLive.Index do
use DashboardWeb, :live_view
@impl true
def mount(_params, _session, socket) do
{:ok, assign(socket, count: 0)}
end
@impl true
def render(assigns) do
~H"""
<div class="dashboard-card">
<h1>Total Request</h1>
<p class="metric"><%= @count %></p>
<button phx-click="increment">Tambah</button>
</div>
"""
end
@impl true
def handle_event("increment", _params, socket) do
{:noreply, update(socket, :count, &(&1 + 1))}
end
endAlur kerjanya sederhana. mount/3 menaruh nilai awal count ke assigns. render/1 membaca @count dan menghasilkan markup. Ketika tombol diklik, browser mengirim event increment melalui WebSocket, lalu handle_event/3 memperbarui assigns. Setiap perubahan state memicu re-render dan pengiriman diff ke browser.
Yang menarik, render/1 berperilaku sebagai pure function dari assigns. Input yang sama selalu menghasilkan output yang sama, sehingga hasilnya mudah diprediksi dan diuji. Tidak ada manipulasi DOM manual, tidak ada query selector, dan tidak ada JavaScript yang harus kita tulis. Hasilnya adalah counter yang bertambah setiap kali tombol diklik tanpa reload halaman.
Menangani Event Form dan Update State secara Real-Time
Dashboard jarang hanya menampilkan angka statis. Kita biasanya butuh filter untuk memilih metrik atau rentang waktu. LiveView menangani kebutuhan ini lewat atribut phx-change dan phx-submit pada elemen form.
defmodule DashboardWeb.DashboardLive.Index do
use DashboardWeb, :live_view
@impl true
def mount(_params, _session, socket) do
{:ok, assign(socket,
filter: %{"metric" => "requests", "range" => "24h"},
metrics: load_metrics("requests", "24h"))}
end
@impl true
def render(assigns) do
~H"""
<form phx-change="filter" phx-submit="apply">
<select name="metric">
<option value="requests">Requests</option>
<option value="errors">Errors</option>
</select>
<input type="text" name="range" value={@filter["range"]} />
<button type="submit">Terapkan</button>
</form>
<ul>
<li :for={point <- @metrics}><%= point %></li>
</ul>
"""
end
@impl true
def handle_event("filter", params, socket) do
{:noreply, assign(socket, filter: params)}
end
@impl true
def handle_event("apply", params, socket) do
{:noreply, assign(socket, metrics: load_metrics(params["metric"], params["range"]))}
end
defp load_metrics(metric, range) do
# query database atau aggregasi data
["#{metric}:#{range}:120", "#{metric}:#{range}:98"]
end
endphx-change mengirim payload form setiap kali nilai input berubah, sehingga kita bisa menyimpan filter terbaru ke assigns. phx-submit menangani pengiriman penuh dan memicu pemuatan ulang data. Kedua event berjalan melalui handle_event/3 yang sama, membedakan aksi berdasarkan nama event.
Karena state berada di server, perubahan langsung tercermin di semua client yang terhubung. Ketika satu tab mengubah filter, tab lain yang membuka dashboard yang sama ikut diperbarui. Proses ini terjadi tanpa satu baris JavaScript pun, karena seluruh sinkronisasi ditangani oleh koneksi WebSocket.
Menyebarkan Update ke Banyak User dengan Phoenix PubSub
LiveView tunggal hanya memperbarui satu browser. Dashboard tim membutuhkan semua anggota melihat data yang sama secara bersamaan. Di sinilah Phoenix PubSub berperan, dengan pola subscribe dan broadcast.
defmodule DashboardWeb.DashboardLive.Index do
use DashboardWeb, :live_view
@topic "dashboard:metrics"
@impl true
def mount(_params, _session, socket) do
if connected?(socket) do
Phoenix.PubSub.subscribe(Dashboard.PubSub, @topic)
end
{:ok, assign(socket, metrics: load_metrics())}
end
@impl true
def handle_event("refresh", _params, socket) do
Phoenix.PubSub.broadcast(Dashboard.PubSub, @topic, {:metrics_updated, load_metrics()})
{:noreply, socket}
end
@impl true
def handle_info({:metrics_updated, metrics}, socket) do
{:noreply, assign(socket, metrics: metrics)}
end
endSetiap LiveView yang terhubung memanggil subscribe pada topic dashboard:metrics. Ketika satu user memicu event refresh, server melakukan broadcast pesan ke topic tersebut. Semua LiveView yang subscribe menerima pesan lewat handle_info/2 dan memperbarui assigns masing-masing.
Alur ini memastikan satu sumber kebenaran. Data dihitung sekali di server, lalu disebarkan ke semua client. Kita tidak perlu memikirkan sinkronisasi antar browser, karena PubSub menangani distribusi pesan secara internal. Perubahan di satu tab akan muncul serentak di semua tab lain yang membuka dashboard.

Gambar: Pola Publish-Subscribe di mana publisher mengirim pesan ke broker, dan broker meneruskan ke semua subscriber yang berlangganan — Sumber: Wikimedia Commons
Best Practices dan Batasan LiveView untuk Dashboard
LiveView paling cocok untuk dashboard, admin panel, dan alat kolaborasi dengan interaktivitas sedang. Pola server-driven ini unggul ketika logika bisnis kompleks dan kebutuhan real-time tinggi. Namun ada batas yang perlu kita sadari.
Untuk rendering canvas yang kompleks, library chart pihak ketiga, atau animasi client yang berat, kita tetap membutuhkan JavaScript hooks. LiveView menyediakan mekanisme hook untuk kasus seperti ini, sehingga kita tidak harus memilih salah satu secara kaku.
Performa juga perlu dijaga. render/1 harus tetap ringan, hindari query mahal di dalam fungsi render, dan gunakan assign secara efisien. Query database sebaiknya dilakukan di mount/3 atau handle_event/3, bukan di render/1. Kita juga perlu memantau kesehatan koneksi dan menangani reconnection dengan baik, misalnya dengan menampilkan indikator saat socket terputus.
Dashboard real-time tanpa JavaScript bukan lagi hal yang mustahil. Dengan Phoenix LiveView, kita bisa membangun antarmuka server-driven yang cepat, konsisten, dan mudah dirawat. Jika ingin mendalami Elixir dan Phoenix secara terstruktur, Rumah Coding menyediakan kursus Web Development yang membahas arsitektur real-time dari dasar hingga production-grade. Bergabunglah dan bangun aplikasi real-time pertamamu bersama kami.