Fitur realtime memungkinkan halaman Next.js menampilkan perubahan data tanpa menunggu pengguna melakukan refresh. Dengan Supabase Realtime, aplikasi dapat berlangganan perubahan pada tabel PostgreSQL, lalu memperbarui state React ketika terjadi event INSERT, UPDATE, atau DELETE.
Contoh yang digunakan adalah daftar tugas. Ketika status tugas berubah dari pending menjadi done di perangkat atau proses lain, semua klien yang berhak melihat data tersebut dapat menerima perubahan dan memperbarui tampilan secara otomatis.
Arsitektur Next.js dan Supabase Realtime
Alur dasarnya terdiri dari beberapa komponen:
- PostgreSQL Supabase menyimpan data tugas atau komentar.
- Supabase Realtime memantau perubahan database dan mengirim event melalui koneksi realtime.
- Komponen Client Next.js membuat subscription menggunakan Supabase browser client.
- Callback event memperbarui state React secara lokal.
Subscription realtime harus dijalankan di Client Component karena membutuhkan efek samping browser dan koneksi realtime. Server Component tetap dapat digunakan untuk mengambil data awal, tetapi subscription sebaiknya berada pada komponen dengan direktif "use client".
Menyiapkan tabel dan akses Realtime
Misalnya tabel tasks memiliki kolom id, title, status, user_id, dan updated_at. Tabel tersebut harus diaktifkan untuk Realtime melalui pengaturan replication di Supabase atau konfigurasi publication yang sesuai. Pastikan perubahan konfigurasi sudah diterapkan sebelum menguji subscription.
Gunakan environment variable untuk URL dan anonymous key Supabase. Anonymous key boleh digunakan di browser, tetapi aksesnya tetap harus dibatasi oleh Row Level Security. Jangan pernah menaruh service_role key di kode frontend.
npm install @supabase/supabase-js
// lib/supabase/browser.ts
import { createClient } from '@supabase/supabase-js'
export const supabase = createClient(
process.env.NEXT_PUBLIC_SUPABASE_URL!,
process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY!
)
Membuat subscription tabel di Next.js
Contoh berikut mengambil daftar tugas, kemudian berlangganan event pada tabel tasks. Event difilter berdasarkan user_id agar klien tidak menerima perubahan dari pengguna lain.
"use client"
import { useEffect, useState } from 'react'
import { supabase } from '@/lib/supabase/browser'
type Task = {
id: string
title: string
status: 'pending' | 'done'
user_id: string
updated_at: string
}
type Props = {
userId: string
}
export function TaskList({ userId }: Props) {
const [tasks, setTasks] = useState<Task[]>([])
const [connectionState, setConnectionState] = useState('CONNECTING')
useEffect(() => {
let active = true
const loadTasks = async () => {
const { data, error } = await supabase
.from('tasks')
.select('id, title, status, user_id, updated_at')
.eq('user_id', userId)
.order('updated_at', { ascending: false })
if (!error && active) {
setTasks(data ?? [])
}
}
const channel = supabase
.channel(`tasks:user:${userId}`)
.on(
'postgres_changes',
{
event: '*',
schema: 'public',
table: 'tasks',
filter: `user_id=eq.${userId}`
},
(payload) => {
setTasks((current) => {
if (payload.eventType === 'INSERT') {
const newTask = payload.new as Task
if (current.some((task) => task.id === newTask.id)) {
return current
}
return [newTask, ...current]
}
if (payload.eventType === 'UPDATE') {
const updatedTask = payload.new as Task
return current.map((task) =>
task.id === updatedTask.id ? updatedTask : task
)
}
if (payload.eventType === 'DELETE') {
const deletedTask = payload.old as Partial<Task>
return current.filter((task) => task.id !== deletedTask.id)
}
return current
})
}
)
.subscribe((status) => {
setConnectionState(status)
})
loadTasks()
return () => {
active = false
void supabase.removeChannel(channel)
}
}, [userId])
return (
<section>
<p>Realtime: {connectionState}</p>
<ul>
{tasks.map((task) => (
<li key={task.id}>
{task.title} — {task.status}
</li>
))}
</ul>
<section>
)
}
Callback setTasks menggunakan bentuk functional update agar selalu bekerja berdasarkan state terbaru. Ini penting karena event realtime dapat datang berdekatan dan closure React dapat menyimpan nilai state lama.
Memilih event dan filter yang tepat
Jika hanya membutuhkan perubahan status tugas, subscription dapat dibatasi ke event UPDATE. Filter juga dapat menggunakan kolom tertentu, misalnya id=eq.<task-id>. Pembatasan ini mengurangi event yang harus diproses oleh browser.
const channel = supabase
.channel(`task:${taskId}`)
.on(
'postgres_changes',
{
event: 'UPDATE',
schema: 'public',
table: 'tasks',
filter: `id=eq.${taskId}`
},
(payload) => {
const task = payload.new as Task
setTask(task)
}
)
.subscribe()
Untuk komentar, pola yang sama dapat digunakan dengan tabel comments dan filter seperti task_id=eq.<task-id>. Jika nama kolom atau nilai filter berasal dari input pengguna, validasi terlebih dahulu dan jangan membangun filter dari nilai yang tidak dipercaya secara sembarangan.
Cleanup channel dan lifecycle React
Setiap pemanggilan supabase.channel() membuat channel yang harus dilepas ketika komponen unmount atau dependency berubah. Tanpa cleanup, navigasi antarhalaman dapat meninggalkan subscription lama. Akibatnya satu perubahan database bisa memicu beberapa callback dan menghasilkan duplikasi data atau beban koneksi yang tidak perlu.
Gunakan nama channel yang stabil untuk konteks yang sama, lalu panggil supabase.removeChannel(channel) pada fungsi cleanup. Dependency effect harus mencakup nilai yang digunakan subscription, misalnya userId atau taskId.
Pengaruh Row Level Security terhadap Realtime
Realtime bukan mekanisme untuk melewati otorisasi database. Data yang diterima klien harus tetap dibatasi oleh policy Supabase dan identitas pengguna yang sedang login. Aktifkan RLS pada tabel dan buat policy yang hanya mengizinkan pengguna mengakses tugasnya sendiri atau anggota proyek yang relevan.
alter table public.tasks enable row level security;
create policy "Users can read their own tasks"
on public.tasks
for select
to authenticated
using (user_id = auth.uid());
create policy "Users can update their own tasks"
on public.tasks
for update
to authenticated
using (user_id = auth.uid())
with check (user_id = auth.uid());
Policy SELECT sangat penting untuk data yang dibaca melalui browser dan event perubahan database. Policy INSERT, UPDATE, dan DELETE tetap diperlukan sesuai operasi yang diizinkan aplikasi. Detail perilaku event, terutama untuk baris yang dihapus atau perubahan yang tidak lagi memenuhi policy, bergantung pada konfigurasi dan kemampuan Realtime yang digunakan; karena itu, jangan menjadikan event frontend sebagai satu-satunya lapisan keamanan.
Catatan keamanan: sembunyikan data sensitif dari query dan payload frontend. RLS membatasi akses, tetapi desain tabel dan kolom yang dikirim ke browser tetap harus mengikuti prinsip least privilege.
Menangani reconnect dan sinkronisasi ulang
Koneksi WebSocket dapat terputus karena perpindahan jaringan, laptop sleep, proxy, atau tab browser yang ditangguhkan. Supabase client menangani mekanisme koneksi channel, tetapi aplikasi tetap perlu mengantisipasi status seperti CHANNEL_ERROR, TIMED_OUT, atau CLOSED.
Strategi yang aman adalah melakukan resync dengan mengambil snapshot terbaru ketika channel kembali tersambung atau ketika koneksi gagal dalam waktu tertentu. Subscription hanya boleh dibuat sekali untuk konteks yang sama; hindari membuat channel baru pada setiap error tanpa menghapus channel lama.
const refreshTasks = async () => {
const { data } = await supabase
.from('tasks')
.select('id, title, status, user_id, updated_at')
.eq('user_id', userId)
if (data) setTasks(data)
}
const channel = supabase
.channel(`tasks:user:${userId}`)
.on('postgres_changes', {
event: '*',
schema: 'public',
table: 'tasks',
filter: `user_id=eq.${userId}`
}, handleChange)
.subscribe(async (status) => {
setConnectionState(status)
if (status === 'SUBSCRIBED') {
await refreshTasks()
}
})
Resync setelah SUBSCRIBED membantu memperbaiki state yang mungkin berubah ketika klien belum memiliki koneksi. Untuk aplikasi dengan kebutuhan konsistensi lebih tinggi, gunakan kolom updated_at, lakukan merge berdasarkan ID, atau lakukan refetch penuh setelah reconnect. Subscription realtime sebaiknya dipandang sebagai notifikasi perubahan, bukan sebagai log yang menjamin aplikasi menerima setiap event tanpa celah.
Mencegah duplikasi dan konflik state
Duplikasi dapat muncul ketika effect berjalan ulang, channel lama belum dibersihkan, proses reconnect memicu sinkronisasi, atau event insert diproses dua kali. Beberapa praktik yang membantu:
- Gunakan
idsebagai identitas unik saat melakukan insert atau merge. - Untuk update, ganti item berdasarkan ID, bukan menambahkan item baru.
- Untuk delete, hapus berdasarkan ID dari
payload.oldjika tersedia. - Setelah refetch, gunakan hasil server sebagai snapshot yang otoritatif.
- Jika urutan event penting, bandingkan
updated_atatau versi row sebelum menerima update yang lebih lama.
Operasi optimistis, misalnya langsung menampilkan status done sebelum request selesai, juga harus memiliki strategi rollback. Jika update database gagal atau policy menolaknya, lakukan refetch agar state UI kembali sesuai server.
Performa dan batasan subscription
Subscription per baris atau per komponen memang mudah dipahami, tetapi dapat menjadi mahal ketika jumlah pengguna dan komponen bertambah. Pertimbangkan hal berikut:
- Gunakan satu channel untuk satu konteks halaman atau proyek jika beberapa komponen membaca dataset yang sama.
- Filter berdasarkan kolom yang relevan, seperti
project_idatautask_id. - Berlangganan hanya saat halaman atau panel benar-benar terlihat.
- Jangan mengirim seluruh row jika UI hanya membutuhkan beberapa kolom; query awal tetap dapat memilih kolom secara eksplisit.
- Debounce atau kelompokkan pembaruan UI jika event datang sangat sering, misalnya pada dashboard metrik.
- Untuk daftar besar, gunakan pagination atau virtualisasi dan jangan memuat seluruh dataset pada setiap event.
Perlu dibedakan antara filter di subscription dan otorisasi. Filter mengurangi event yang diproses, sedangkan RLS menentukan data yang boleh diakses. Keduanya harus digunakan bersama.
Kapan polling lebih tepat digunakan?
Realtime bukan selalu pilihan terbaik. Polling dapat lebih sederhana dan lebih stabil jika data jarang berubah, pengguna tidak membutuhkan latensi rendah, deployment memiliki pembatasan WebSocket, atau halaman bersifat publik dengan banyak pengunjung anonim.
Pilih polling ketika kebutuhan hanya berupa penyegaran berkala, misalnya memeriksa status proses setiap 30 detik. Pilih Supabase Realtime ketika perubahan harus terlihat segera, seperti status tugas kolaboratif, komentar, notifikasi, atau indikator kehadiran. Dalam beberapa kasus, pendekatan hybrid lebih sesuai: realtime untuk perubahan cepat dan polling periodik sebagai mekanisme rekonsiliasi.
Checklist debugging
- Pastikan tabel sudah diaktifkan untuk Supabase Realtime.
- Periksa schema dan nama tabel pada konfigurasi
postgres_changes. - Pastikan komponen berjalan di client dan environment variable tersedia di browser.
- Periksa status subscription pada callback
subscribe. - Uji policy RLS dengan pengguna yang benar-benar login.
- Pastikan operasi database berhasil; event tidak akan memperbaiki transaksi yang gagal.
- Periksa apakah effect membuat channel berulang dan apakah cleanup dipanggil.
- Lakukan query ulang setelah reconnect untuk mendeteksi event yang terlewat.
Dengan memisahkan pengambilan snapshot awal, subscription event, pembaruan state berbasis ID, cleanup channel, dan rekonsiliasi setelah reconnect, fitur realtime Next.js dengan Supabase Realtime dapat dibuat lebih aman dan mudah dipelihara. RLS tetap menjadi fondasi otorisasi, sedangkan subscription berfungsi sebagai jalur pemberitahuan perubahan untuk memperbarui antarmuka.
Komentar
0 komentar
Masuk ke akun kamu untuk ikut berkomentar.
Belum ada komentar
Jadilah yang pertama ikut berdiskusi!