Hydration error React SSR di Bun biasanya bukan masalah pada Bun itu sendiri, melainkan pada hasil render awal yang berbeda antara server dan browser. Saat HTML dari server tidak cocok dengan output render pertama di klien, React akan menampilkan warning hydration, UI bisa flicker, class atau tema berubah sesaat, dan event handler kadang terasa tidak konsisten.
Kasus paling umum terjadi ketika komponen membaca localStorage, window, matchMedia, waktu saat ini, atau preferensi tema langsung saat render awal. Di server, nilai-nilai itu tidak tersedia atau berbeda. Akibatnya, markup yang dikirim saat SSR tidak sama dengan markup yang dihasilkan browser saat proses hydration.
Apa yang Sebenarnya Terjadi saat Hydration
Pada SSR, server menghasilkan HTML awal agar halaman cepat tampil dan bisa diindeks mesin pencari. Setelah itu, React di browser melakukan hydration: React tidak membuat UI dari nol, tetapi mencoba menempelkan event listener dan state ke HTML yang sudah ada.
Hydration hanya berjalan mulus jika output render awal di browser sama dengan HTML hasil render server. Jika berbeda, React bisa:
- menampilkan warning seperti Text content did not match atau Hydration failed,
- membuang sebagian tree dan merender ulang di klien,
- menyebabkan flicker karena DOM berubah setelah mount,
- membuat perilaku UI terasa aneh karena node yang di-hydrate tidak sesuai ekspektasi.
Intinya: SSR membutuhkan deterministic first render. Render pertama di server dan browser harus menghasilkan markup yang sama.
Gejala Nyata yang Sering Muncul
1. Warning hydration di console
Gejala paling jelas adalah warning React di browser console. Pesannya bisa berbeda-beda, tetapi maknanya sama: HTML awal tidak cocok dengan hasil render klien.
2. UI flicker saat halaman pertama kali tampil
Contohnya tema awal tampil terang, lalu berubah ke gelap sesaat setelah JavaScript jalan. Atau label tombol awal menampilkan Login, lalu berubah ke Logout setelah state dari browser terbaca.
3. Class atau atribut berubah setelah mount
Kasus umum: className pada <html> atau root element berubah karena tema dihitung dari localStorage atau matchMedia di klien.
4. Event handler terasa aneh
Sering kali ini bukan event handler yang rusak, tetapi node DOM yang di-hydrate tidak sesuai dengan tree yang diharapkan React. Akibat mismatch, React bisa mengganti subtree tertentu, dan ini membuat interaksi terasa janggal.
Root Cause: Initial State Server dan Browser Tidak Sama
Akar masalahnya hampir selalu sama: state awal bergantung pada environment browser, sementara SSR berjalan di server. Di Bun, seperti runtime server lain, kode SSR tidak berjalan dalam environment browser penuh. Jika logika render membaca sumber state yang hanya akurat di klien, hasilnya akan berbeda.
Sumber mismatch yang paling sering:
localStorageatausessionStorage,window,document,navigator,matchMedia('(prefers-color-scheme: dark)'),Date.now(),new Date(), timezone lokal,- ukuran viewport seperti
window.innerWidth, - random value seperti
Math.random()saat render.
Bun bukan penyebab mismatch tersebut, tetapi saat Anda menjalankan React SSR di Bun, gejalanya tetap muncul karena prinsip hydration React tetap sama: server output harus cocok dengan client first render.
Contoh Kode yang Memicu Mismatch
Membaca localStorage langsung saat inisialisasi state
import { useState } from 'react';
export function ThemeToggle() {
const [theme, setTheme] = useState(() => {
return localStorage.getItem('theme') || 'light';
});
return (
<button onClick={() => setTheme(theme === 'dark' ? 'light' : 'dark')}>
Tema: {theme}
</button>
);
}Di browser, ini mungkin bekerja. Di SSR, ada dua masalah:
localStoragetidak tersedia di server, sehingga bisa error langsung,- kalaupun diberi guard, nilai di server kemungkinan berbeda dengan browser.
Guard browser API, tetapi tetap mismatch
import { useState } from 'react';
export function ThemeToggle() {
const [theme, setTheme] = useState(() => {
if (typeof window !== 'undefined') {
return localStorage.getItem('theme') || 'light';
}
return 'light';
});
return <div className={theme}>Tema aktif: {theme}</div>;
}Kode di atas tidak crash saat SSR, tetapi masih bisa menyebabkan hydration error React SSR di Bun. Mengapa? Karena server merender light, sedangkan browser pada render pertama bisa langsung membaca dark dari localStorage. Markup awal jadi berbeda.
Menggunakan waktu saat render
export function Greeting() {
const hour = new Date().getHours();
const text = hour < 18 ? 'Selamat siang' : 'Selamat malam';
return <p>{text}</p>;
}Jika server dan browser memiliki timezone berbeda, atau hydration terjadi di detik berbeda pada boundary tertentu, teks yang dihasilkan bisa berubah.
Menggunakan media query saat render
export function Sidebar() {
const isMobile = window.matchMedia('(max-width: 768px)').matches;
return isMobile ? <MobileMenu /> : <DesktopMenu />;
}Ini jelas tidak aman untuk SSR. Bahkan jika ditambah guard, hasil server dan browser tetap berpotensi berbeda.
Cara Mengisolasi Bug dengan Cepat
1. Cari komponen yang hasil rendernya bergantung pada browser
Mulailah dari area UI yang flicker atau berubah sesaat setelah mount: tema, navbar login state, sidebar responsif, jam, greeting, bahasa, atau preferensi user.
2. Audit semua akses environment-sensitive
Telusuri penggunaan:
window,document,navigator,localStorage,sessionStorage,Date,Intldengan timezone lokal,matchMedia, ukuran viewport,- random generator saat render.
Jika dipakai di body komponen atau initializer useState, curigai dulu.
3. Bandingkan output server vs render pertama browser
Cara praktisnya:
- lihat HTML hasil SSR di tab View Source atau response network,
- bandingkan dengan DOM setelah hydration,
- periksa teks, atribut, dan
classNameyang berubah.
Fokus pada elemen pertama yang mismatch. Biasanya warning React juga memberi petunjuk subtree mana yang bermasalah.
4. Tambahkan logging yang membedakan server dan client
const isServer = typeof window === 'undefined';
console.log('[theme-render]', {
env: isServer ? 'server' : 'client',
theme
});Jangan tinggalkan logging seperti ini di produksi terlalu lama, tetapi saat diagnosis ini sangat membantu untuk memastikan apakah render awal menghasilkan nilai berbeda.
5. Sederhanakan sampai mismatch hilang
Nonaktifkan sementara logika yang membaca browser state. Jika warning hilang, Anda sudah menemukan penyebabnya. Setelah itu, terapkan pola perbaikan yang tepat, bukan sekadar menambahkan guard.
Pola Perbaikan yang Aman
1. Defer pembacaan browser state ke useEffect
Jika state memang hanya bisa diketahui di klien, jangan gunakan untuk menentukan markup SSR. Render nilai fallback yang stabil, lalu baca nilai browser setelah mount.
import { useEffect, useState } from 'react';
export function ThemeToggle() {
const [theme, setTheme] = useState('light');
useEffect(() => {
const saved = localStorage.getItem('theme');
if (saved === 'light' || saved === 'dark') {
setTheme(saved);
}
}, []);
useEffect(() => {
document.documentElement.dataset.theme = theme;
localStorage.setItem('theme', theme);
}, [theme]);
return (
<button onClick={() => setTheme(theme === 'dark' ? 'light' : 'dark')}>
Tema: {theme}
</button>
);
}Mengapa ini bekerja? Karena server dan browser sama-sama merender light pada render pertama. Setelah hydration selesai, useEffect berjalan di klien dan memperbarui state.
Trade-off: ada potensi flicker jika nilai sebenarnya di browser adalah dark. Jadi ini aman dari hydration mismatch, tetapi belum tentu ideal untuk UX.
2. Guard akses browser API, tetapi jangan jadikan hasilnya penentu first render SSR
Guard seperti typeof window !== 'undefined' berguna untuk mencegah crash di server, tetapi bukan solusi penuh untuk hydration mismatch. Gunakan guard bersama strategi render yang konsisten.
const canUseBrowser = typeof window !== 'undefined';Anggap guard hanya sebagai proteksi runtime, bukan mekanisme sinkronisasi state server-client.
3. Kirim initial state dari server jika state bisa diketahui lebih awal
Ini pendekatan terbaik jika Anda ingin menghindari flicker dan menjaga HTML awal tetap benar. Jika tema, locale, atau preferensi lain bisa ditentukan di server dari cookie, header, atau session, kirim nilai itu ke React sebagai initial props.
// pseudo-code SSR
const initialTheme = readThemeFromCookie(request) || 'light';
const html = renderToString(<App initialTheme={initialTheme} />);import { useState } from 'react';
export function App({ initialTheme }) {
const [theme, setTheme] = useState(initialTheme);
return <div data-theme={theme}>...</div>;
}Mengapa ini lebih baik? Karena server dan browser memulai dari nilai yang sama. Hydration jadi stabil, dan pengguna tidak melihat perubahan tema setelah mount.
Cocok untuk:
- tema dari cookie,
- locale dari request,
- status autentikasi dari session server,
- feature flag yang diputuskan di backend.
Tidak cocok untuk:
- viewport aktual browser,
- hasil
matchMediayang hanya akurat di klien, - nilai yang benar-benar baru diketahui setelah browser aktif.
4. Pisahkan komponen client-only bila SSR tidak memberi nilai tambah
Jika sebuah komponen sepenuhnya bergantung pada browser state dan tidak penting untuk SEO atau first paint, jadikan komponen tersebut hanya dirender di klien.
Contoh kasus:
- widget preferensi tampilan,
- panel layout berdasarkan viewport,
- komponen yang sangat interaktif dan bergantung pada API browser.
Pemisahan ini bisa dilakukan sesuai pola framework yang Anda gunakan. Prinsipnya sama: jangan ikutkan subtree itu dalam SSR jika markup awalnya hampir pasti berbeda.
Trade-off: hydration mismatch hilang, tetapi HTML awal untuk komponen itu kosong atau placeholder. Ini bisa memengaruhi UX awal dan SEO jika komponen tersebut berisi konten penting.
5. Gunakan placeholder yang stabil untuk state yang belum diketahui
Untuk data seperti waktu lokal user atau ukuran viewport, sering kali lebih baik menampilkan placeholder netral saat SSR, lalu isi nilainya setelah mount.
import { useEffect, useState } from 'react';
export function LocalTime() {
const [time, setTime] = useState(null);
useEffect(() => {
setTime(new Date().toLocaleTimeString());
}, []);
return <span>{time ?? '--:--:--'}</span>;
}Dengan cara ini, first render tetap konsisten. Anda mengorbankan sedikit kelengkapan tampilan awal demi stabilitas hydration.
Kasus Khusus: Tema Gelap dan Class pada Root Element
Masalah tema sering paling terlihat karena memengaruhi seluruh halaman. Pola yang sering salah adalah menghitung tema dari localStorage saat render, lalu langsung menaruh class dark atau light pada root element.
Jika server merender light tetapi browser mengubah ke dark saat hydration, pengguna akan melihat flash tema. Untuk kasus ini, pilihan perbaikannya biasanya:
- Terbaik: simpan tema di cookie dan baca saat SSR agar server sudah tahu tema yang benar.
- Cukup aman: render tema default yang stabil, lalu sinkronkan di
useEffect. - Praktis untuk komponen kecil: jadikan toggle atau panel preferensi sebagai client-only.
Jika tema memengaruhi seluruh dokumen, pendekatan server-driven via cookie biasanya paling baik untuk UX karena meminimalkan flicker.
Kesalahan Umum yang Sering Terjadi
- Mengira guard window sudah cukup. Guard hanya mencegah error, bukan mismatch.
- Menggunakan Date atau Math.random saat render. Keduanya membuat output sulit deterministik.
- Menghitung layout dari viewport saat SSR. Server tidak tahu ukuran browser yang sebenarnya.
- Mencampur state autentikasi server dan state browser tanpa fallback yang jelas.
- Memperbaiki warning dengan menyembunyikan komponen, tanpa memahami sumber perbedaannya.
Checklist Debugging Hydration Error React SSR di Bun
- Apakah ada akses ke
window,document,navigator,localStorage, atausessionStoragesaat render? - Apakah initializer
useStatemembaca browser API? - Apakah ada penggunaan
new Date(), timezone lokal, atauMath.random()saat render? - Apakah server merender tema, class, atau teks yang berbeda dari browser?
- Apakah komponen bergantung pada
matchMediaatau ukuran viewport? - Apakah warning menunjuk ke subtree tertentu yang berubah setelah mount?
- Bisakah initial state dikirim dari server melalui cookie, session, atau props SSR?
- Jika state hanya diketahui di klien, apakah logikanya sudah dipindah ke
useEffect? - Jika komponen tidak penting untuk SSR, apakah lebih tepat dijadikan client-only?
Trade-off UX, SEO, dan Performa
Defer ke useEffect
- Kelebihan: sederhana, aman dari mismatch.
- Kekurangan: bisa menimbulkan flicker dan perubahan UI setelah mount.
- Cocok untuk: preferensi minor yang tidak kritis saat first paint.
Kirim initial state dari server
- Kelebihan: SSR dan hydration konsisten, UX lebih halus, minim flicker.
- Kekurangan: butuh integrasi request, cookie, atau session.
- Cocok untuk: tema, auth state, locale, feature flag.
Client-only component
- Kelebihan: menghilangkan mismatch pada komponen yang sulit dibuat deterministik.
- Kekurangan: HTML awal berkurang, bisa berdampak ke SEO dan perceived performance.
- Cocok untuk: widget interaktif yang tidak penting untuk indexing atau first paint.
Penutup
Jika Anda menemukan hydration error React SSR di Bun, fokuslah pada satu pertanyaan: apakah render pertama di server dan browser menghasilkan markup yang sama? Dalam banyak kasus, sumber masalahnya adalah state awal yang dibaca dari environment browser seperti localStorage, window, tema sistem, waktu lokal, atau media query.
Perbaikan yang tepat bergantung pada sifat state tersebut. Jika state bisa diketahui di server, kirim sebagai initial state agar SSR tetap akurat. Jika hanya bisa diketahui di browser, tunda pembacaannya ke useEffect atau pisahkan komponen menjadi client-only. Dengan pendekatan ini, Anda tidak hanya menghilangkan warning, tetapi juga mengurangi flicker, menjaga interaksi tetap konsisten, dan mempertahankan manfaat SSR secara seimbang.
Komentar
0 komentar
Masuk ke akun kamu untuk ikut berkomentar.
Belum ada komentar
Jadilah yang pertama ikut berdiskusi!