Debug hydration Supabase Auth di SSR hampir selalu bermuara pada satu masalah inti: server merender UI dengan status auth A, tetapi browser melakukan hydration dengan status auth B. Akibatnya, komponen login bisa sempat tampil lalu hilang, tombol akun berubah mendadak, atau bahkan muncul warning render mismatch.
Masalah ini umum pada aplikasi SSR modern yang memakai Supabase Auth karena session bisa berasal dari cookie, refresh token, penyimpanan browser, atau event auth yang baru sinkron setelah halaman dimuat. Solusinya bukan sekadar menambahkan loading spinner di mana-mana, melainkan memastikan sumber kebenaran auth saat render awal jelas, konsisten, dan dipisahkan dengan benar antara server dan client.
Gejala yang Biasanya Muncul
Bug ini sering terlihat kecil, tetapi dampaknya cukup mengganggu pengalaman pengguna dan bisa memicu perilaku UI yang sulit ditebak.
- UI login berkedip: tombol Login muncul sebentar lalu berubah menjadi avatar atau menu akun.
- Render mismatch: HTML dari server tidak sama dengan hasil render awal di client.
- Protected layout meloncat: halaman privat sempat menampilkan state anonim sebelum data session terbaca.
- Redirect tidak stabil: server menganggap user belum login, tetapi client menganggap sudah login setelah mount.
- Event auth terasa terlambat: listener auth di browser memperbarui UI setelah hydration selesai.
Jika bug hanya muncul saat hard refresh, bukan saat navigasi client-side, kemungkinan besar penyebabnya ada pada perbedaan state auth antara SSR dan hydration.
Root Cause: Mengapa State Auth Bisa Berbeda?
1. Server dan client membaca sumber session yang berbeda
Pada SSR, server biasanya membaca session dari cookie yang ikut dalam request. Sementara di browser, client auth bisa memulihkan session dari state internal SDK, storage browser, atau hasil refresh token. Jika keduanya tidak sinkron pada saat render awal, markup yang dihasilkan juga berbeda.
2. Pembacaan auth dilakukan dari API yang hanya valid di browser
Kesalahan umum adalah langsung memanggil logika pembacaan user di komponen yang dirender pada server, padahal implementasinya bergantung pada window, localStorage, atau listener browser. Server tidak punya akses ke API tersebut, sehingga default state sering jatuh ke unauthenticated.
3. Render kondisional berubah setelah mount
Banyak implementasi memakai pola seperti: jika tidak ada user, tampilkan tombol login. Masalahnya, pada render pertama di browser nilai user bisa masih null, lalu berubah menjadi object session setelah efek client berjalan. Hasilnya, UI berganti setelah hydration dan terlihat berkedip.
4. Initial session tidak dioper dari server ke client
Jika server sudah mengetahui session, tetapi client provider memulai state dari null, maka ada dua fase render yang tidak konsisten. Server merender user login, namun provider di browser memulai dari anonim lalu memperbaiki diri beberapa saat kemudian.
5. Listener auth dianggap sebagai sumber state awal
Listener seperti perubahan auth di client berguna untuk update setelah halaman aktif, tetapi bukan pengganti state awal SSR. Jika seluruh UI menunggu listener ini untuk mengetahui status login, hampir pasti akan ada fase transisi yang memicu flicker.
Alur Render yang Menyebabkan Hydration Bug
Masalahnya lebih mudah dipahami jika dilihat sebagai urutan kejadian:
- Request masuk ke server.
- Server mencoba membaca session dari cookie.
- Server merender HTML berdasarkan hasil tersebut.
- HTML dikirim ke browser dan ditampilkan.
- Client melakukan hydration.
- SDK Supabase di browser memulihkan atau menyegarkan session.
- State auth client ternyata berbeda dari state saat SSR.
- UI dirender ulang dan tampak berkedip atau mismatch.
Kalau langkah 2 dan langkah 6 tidak menghasilkan data yang sama, maka render awal tidak stabil. Tujuan perbaikan adalah membuat langkah 3 dan langkah 6 memakai nilai awal yang identik, atau setidaknya menahan render bagian sensitif sampai status auth benar-benar siap.
Pola Arsitektur yang Aman untuk Supabase Auth di SSR
Tanpa bergantung pada satu framework tertentu, pola yang paling aman biasanya terdiri dari tiga bagian:
- Server membaca session dari request, umumnya lewat cookie.
- Initial session atau initial user diteruskan ke layer client sebagai state awal.
- Client tetap memasang listener auth untuk perubahan setelah hydration, bukan untuk menentukan state awal.
1. Pisahkan client server-side dan browser-side
Jangan pakai satu instance Supabase untuk semua konteks. Di SSR, buat client yang membaca auth dari konteks request. Di browser, buat client yang aman dipakai setelah mount. Pemisahan ini mencegah asumsi keliru bahwa environment server bisa membaca state browser secara langsung.
// pseudo-code arsitektur umum, bukan framework-specific
export function createServerSupabase(requestContext) {
return createSupabaseClient({
cookies: requestContext.cookies
})
}
export function createBrowserSupabase() {
return createSupabaseClient({
// konfigurasi browser-only
})
}2. Ambil session di server sebelum render UI sensitif auth
Server sebaiknya menentukan status awal: apakah user login, siapa user-nya, dan apakah token terlihat valid dari request saat ini. Nilai ini lalu dikirim sebagai data SSR ke layout, provider, atau root state.
// pseudo-code server render
const supabase = createServerSupabase(requestContext)
const session = await getSessionFromRequest(supabase)
return renderPage({
initialSession: session,
initialUser: session?.user ?? null
})3. Gunakan initial session di auth provider client
Provider auth di client harus memulai state dari data yang sama dengan yang dipakai server saat menghasilkan HTML. Dengan begitu, hydration pertama memakai nilai identik, lalu perubahan auth berikutnya ditangani lewat listener.
// pseudo-code client auth provider
function AuthProvider({ initialSession, children }) {
const [session, setSession] = useState(initialSession)
const [isReady, setIsReady] = useState(true)
useEffect(() => {
const supabase = createBrowserSupabase()
const unsubscribe = supabase.auth.onAuthStateChange((_event, nextSession) => {
setSession(nextSession)
})
return () => unsubscribe?.data?.subscription?.unsubscribe?.()
}, [])
return render(children, { session, isReady })
}Poin pentingnya bukan nama API, tetapi prinsipnya: state awal client harus berasal dari hasil SSR, bukan default kosong.
4. Tahan render bagian tertentu jika status auth memang belum final
Ada kasus ketika server tidak bisa atau tidak sebaiknya menentukan session final, misalnya pada edge case refresh token atau middleware auth yang belum selesai. Dalam kondisi seperti ini, lebih aman memakai loading boundary untuk bagian UI yang sangat bergantung pada auth, daripada langsung merender state anonim.
function AuthGate({ authReady, user, children }) {
if (!authReady) {
return <div aria-busy="true">Memuat status akun...</div>
}
if (!user) {
return <LoginButton />
}
return children
}Trade-off-nya jelas: loading boundary mengurangi flicker dan mismatch, tetapi menambah fase menunggu. Ini lebih baik daripada menampilkan UI yang salah lalu menggantinya sepersekian detik kemudian.
Strategi Perbaikan yang Paling Praktis
Gunakan server sebagai sumber kebenaran untuk render pertama
Untuk halaman SSR, keputusan awal tentang login sebaiknya berasal dari request yang sedang diproses. Ini penting agar HTML yang dikirim ke browser sesuai dengan kondisi user pada saat request itu terjadi.
Jangan render berdasarkan browser-only state sebelum mount selesai
Jika komponen membaca user dari hook client yang baru siap setelah mount, jangan langsung pakai nilai default null untuk merender tombol login. Bungkus dengan state unknown atau loading agar tidak terjadi pergantian UI yang drastis.
// hindari pola ini
if (!user) {
return <LoginButton />
}
// lebih aman: bedakan null vs unknown
if (authState === 'loading') {
return <AuthSkeleton />
}
if (authState === 'anonymous') {
return <LoginButton />
}
return <UserMenu />Bedakan state auth: unknown, anonymous, authenticated
Banyak mismatch lahir karena hanya ada dua state: ada user atau tidak ada user. Padahal pada SSR dan hydration, ada fase ketiga yaitu status belum diketahui. Memisahkan tiga state ini membuat alur render lebih stabil.
Sinkronkan data user minimal, bukan seluruh objek berat
Untuk render awal, Anda sering tidak perlu mengirim seluruh detail session ke semua komponen. Cukup kirim informasi minimum yang dibutuhkan seperti isAuthenticated dan identitas user dasar. Ini mengurangi kompleksitas serialisasi dan menghindari ketergantungan UI pada object auth yang bisa berubah bentuk.
Kesalahan Implementasi yang Sering Terjadi
- Membuat browser client di server dan berharap session otomatis tersedia.
- Mengabaikan cookie request saat mengambil session pada SSR.
- Provider auth selalu mulai dari null walaupun server sudah punya session.
- Mengandalkan effect client untuk auth awal sehingga halaman selalu punya fase anonim semu.
- Mencampur logika redirect server dan client tanpa aturan prioritas yang jelas.
- Merender navbar auth-sensitive di root layout tanpa loading strategy, sehingga seluruh halaman terlihat berkedip.
- Mengakses window atau localStorage saat SSR yang memaksa fallback state berbeda.
- Tidak membedakan token expired vs user benar-benar logout, sehingga refresh token dianggap sebagai logout sementara.
Checklist Debugging Hydration Supabase Auth
Saat bug sulit direproduksi, gunakan checklist ini untuk mempersempit sumber masalah:
Verifikasi sumber session
- Apakah server membaca session dari cookie request yang benar?
- Apakah browser membaca session dari instance Supabase yang sesuai untuk client?
- Apakah ada lebih dari satu auth provider atau lebih dari satu client instance yang aktif?
Bandingkan state server dan client
- Log status auth saat SSR: authenticated atau anonymous?
- Log state awal provider di browser sebelum listener auth jalan.
- Log event auth setelah hydration untuk melihat apakah session berubah segera setelah mount.
Periksa kondisi render
- Apakah komponen navbar, header, atau route guard merender berdasarkan
nullyang artinya belum siap? - Apakah ada branch JSX berbeda antara server dan client?
- Apakah placeholder loading sudah konsisten di kedua sisi?
Periksa timing refresh token
- Apakah session di server tampak kosong tetapi langsung terisi di client setelah refresh internal?
- Jika ya, apakah Anda perlu loading boundary atau mekanisme refresh auth lebih awal di server-side path?
Periksa serialisasi data awal
- Apakah
initialSessionbenar-benar dikirim ke client? - Apakah nilainya berubah atau dibuang saat melewati boundary server-client?
- Apakah Anda hanya mengirim bagian data yang aman dan dibutuhkan UI?
Pola UI yang Lebih Stabil
Navigasi publik dengan placeholder netral
Untuk navbar, sering kali lebih aman memakai placeholder netral pada area akun sampai auth siap, misalnya skeleton kecil atau tombol yang tidak langsung mengasumsikan user logout.
Route privat dengan keputusan di server
Jika sebuah halaman memang harus privat, lakukan validasi auth sedini mungkin di server agar user anonim tidak sempat melihat layout privat. Ini lebih baik daripada menampilkan halaman lalu redirect setelah mount.
Komponen campuran: server untuk status awal, client untuk interaksi
Pendekatan yang sehat adalah: server menentukan apakah user login pada request awal, lalu komponen client menangani interaksi lanjutan seperti logout, refresh profile, atau perubahan session real-time.
Kapan Loading Boundary Lebih Tepat daripada Render Langsung?
Loading boundary bukan solusi untuk semua kasus, tetapi tepat digunakan jika:
- status auth awal memang belum bisa dipastikan secara aman saat SSR,
- komponen sangat sensitif terhadap perbedaan auth state,
- flicker lebih buruk daripada menunggu sebentar,
- Anda ingin mencegah hydration mismatch tanpa memaksa state anonim palsu.
Sebaliknya, jika server sudah bisa membaca session dengan andal, lebih baik render langsung dari initial session yang konsisten daripada menambah loading state yang tidak perlu.
Ringkasan Perbaikan yang Aman
Untuk mengatasi UI login berkedip dan mismatch saat memakai Supabase Auth pada SSR, fokuslah pada konsistensi state antara server dan client:
- Baca session dari request di server.
- Jadikan hasilnya sebagai initial session untuk client.
- Pisahkan Supabase client untuk server dan browser.
- Gunakan listener auth di client hanya untuk perubahan setelah hydration.
- Bedakan state loading, anonim, dan terautentikasi.
- Pakai loading boundary hanya di area yang memang belum bisa dirender stabil.
Jika prinsip ini diterapkan, UI tidak perlu lagi tampil sebagai logout lalu berubah menjadi login, dan hydration akan jauh lebih stabil. Pada aplikasi SSR, bug auth seperti ini hampir tidak pernah selesai dengan patch kecil di komponen tunggal; solusinya biasanya ada pada alur data auth dari request server sampai state client awal.
Komentar
0 komentar
Masuk ke akun kamu untuk ikut berkomentar.
Belum ada komentar
Jadilah yang pertama ikut berdiskusi!