Hydration mismatch pada aplikasi SSR muncul ketika markup yang dikirim server tidak cocok dengan hasil render awal di klien. Gejalanya sering terasa membingungkan: teks berubah setelah halaman selesai dimuat, tombol terlihat ada tetapi klik tidak bereaksi, layout meloncat, atau state komponen berbeda dari HTML awal.
Masalah ini jarang selesai jika hanya ditambal dengan percobaan acak. Pendekatan yang lebih efektif adalah understanding-first debugging: pahami mengapa server dan klien menghasilkan output berbeda, lalu perbaiki sumber nondeterminismenya. Dengan pola pikir ini, bug UI SSR biasanya jauh lebih cepat ditemukan dan tidak mudah kambuh.
Apa itu hydration mismatch dan kenapa gejalanya terasa aneh?
Pada arsitektur SSR, server lebih dulu merender HTML agar halaman cepat tampil. Setelah itu, JavaScript di browser melakukan hydration, yaitu menghubungkan markup yang sudah ada dengan komponen dan event handler di klien.
Jika hasil render awal di klien tidak identik dengan HTML dari server, framework biasanya memberi peringatan atau mencoba memperbaiki DOM. Di titik inilah muncul gejala yang tampak tidak konsisten:
- Teks berubah setelah load: server menampilkan nilai A, klien langsung mengganti menjadi nilai B.
- Event handler tidak aktif: elemen terlihat benar, tetapi binding event gagal atau dilepas karena struktur DOM tidak sesuai harapan.
- Layout meloncat: conditional render di klien menghasilkan node berbeda dari server.
- State klien berbeda dari HTML server: input, selected tab, atau status login awal berubah setelah hydration.
Inti masalahnya bukan sekadar “warning hydration”, melainkan dua lingkungan menghasilkan UI awal yang berbeda. Selama penyebab perbedaan itu belum dipahami, perbaikannya biasanya rapuh.
Cara berpikir yang benar: cari sumber perbedaan render awal
Disiplin debugging yang berguna di sini adalah berhenti menebak-nebak dan mulai membandingkan dua hal secara sistematis:
- Apa yang dirender server?
- Apa yang ingin dirender klien pada render pertama, sebelum efek samping berjalan?
Jika keduanya berbeda, hydration mismatch bukan kejutan. Tugas Anda adalah menemukan input apa yang membuat render awal itu tidak deterministik.
Beberapa pertanyaan yang hampir selalu membantu:
- Apakah komponen membaca
window,document, ataulocalStoragesaat render? - Apakah ada
Date.now(),new Date(),Math.random(), atau generator ID acak di render path? - Apakah server dan browser memakai locale atau timezone berbeda?
- Apakah data fetch bisa selesai dalam urutan berbeda antara server dan klien?
- Apakah ada conditional render yang bergantung pada environment browser?
Penyebab umum hydration mismatch pada aplikasi SSR
1. Data nondeterministik saat render
Render SSR harus menghasilkan output yang konsisten untuk input yang sama. Begitu Anda memasukkan nilai yang berubah setiap waktu, mismatch menjadi mungkin.
Contoh buruk:
function Greeting() {
return <p>Render pada: {Date.now()}</p>
}Server dan klien hampir pasti menghasilkan angka berbeda. Hasilnya, teks awal akan berubah saat hydration.
Perbaikan yang lebih aman:
function Greeting({ renderedAt }) {
return <p>Render pada: {renderedAt}</p>
}
// Nilai dibuat sekali di server lalu dikirim sebagai props serialized.Kenapa ini bekerja? Karena server dan klien memakai input awal yang sama, bukan menghitung ulang nilai waktu saat render klien dimulai.
2. Akses window atau localStorage terlalu dini
Browser punya API seperti window dan localStorage, tetapi server tidak. Masalahnya bukan hanya error runtime; kadang Anda memberi fallback di server lalu klien langsung membaca nilai sebenarnya, sehingga render awal berbeda.
Contoh yang memicu mismatch:
function ThemeLabel() {
const theme = localStorage.getItem('theme') || 'light'
return <span>Tema: {theme}</span>
}Di server, ini tidak tersedia. Jika dipaksa dengan guard sederhana:
function ThemeLabel() {
const theme = typeof window === 'undefined'
? 'light'
: localStorage.getItem('theme') || 'light'
return <span>Tema: {theme}</span>
}Server mungkin merender light, tetapi klien pada render pertama merender dark. Hasilnya tetap mismatch.
Pendekatan yang lebih aman:
function ThemeLabel() {
const [theme, setTheme] = useState('light')
useEffect(() => {
const saved = localStorage.getItem('theme')
if (saved) setTheme(saved)
}, [])
return <span>Tema: {theme}</span>
}Trade-off-nya, user mungkin melihat nilai default sesaat sebelum state diperbarui. Jika itu tidak bisa diterima, lebih baik kirim nilai tema dari server melalui cookie atau jadikan bagian itu client-only.
3. Perbedaan locale dan timezone
Format tanggal, angka, atau mata uang bisa berbeda antara environment server dan browser. Ini penyebab yang sering terlewat karena kode terlihat “benar”.
function OrderDate({ createdAt }) {
return <p>{new Date(createdAt).toLocaleString()}</p>
}Jika server berjalan dengan timezone UTC dan browser user di WIB, string hasil format dapat berbeda. Browser lalu mengganti teks setelah hydration.
Strategi perbaikan:
- Format data di server dan kirim string final ke klien.
- Atau tentukan locale/timezone secara eksplisit jika memang harus diformat di kedua sisi.
- Untuk informasi yang memang bergantung pada locale pengguna, pertimbangkan render placeholder lalu hitung di klien.
Prinsipnya: jangan biarkan server dan klien menebak aturan format masing-masing jika hasil visual harus identik saat hydration.
4. Random ID atau key yang berubah
ID acak pada render awal bisa merusak asosiasi elemen, label, atau struktur internal DOM.
function SearchBox() {
const id = Math.random().toString(36).slice(2)
return (
<>
<label htmlFor={id}>Cari</label>
<input id={id} />
</>
)
}Server dan klien menghasilkan ID berbeda. Dampaknya bisa berupa warning hydration, relasi label-input yang berubah, atau perilaku fokus yang aneh.
Solusi terbaik adalah memakai ID stabil dari data atau mekanisme framework yang memang dirancang menghasilkan ID konsisten lintas SSR dan klien.
5. Race condition pada fetch data
Masalah ini sering muncul ketika data awal dari server belum sinkron dengan fetch ulang di klien. Misalnya, server merender daftar berdasarkan snapshot A, tetapi browser langsung fetch dan menerima snapshot B sebelum hydration selesai atau tepat sesudahnya.
Akibatnya, user melihat daftar berubah mendadak, jumlah item berbeda, atau urutan elemen berpindah. Tidak semua kasus memicu warning hydration, tetapi akar masalahnya sama: render awal tidak disepakati.
Yang perlu dijaga:
- Gunakan data SSR sebagai initial state di klien.
- Jangan fetch ulang dengan parameter berbeda pada mount tanpa alasan jelas.
- Jika perlu revalidasi, lakukan setelah hydration dengan transisi yang aman.
6. Conditional render berbasis environment klien
Contoh klasik:
function Banner() {
const isMobile = window.innerWidth < 768
return isMobile ? <MobileBanner /> : <DesktopBanner />
}Server tidak tahu lebar viewport browser secara pasti. Jika server merender versi desktop tetapi klien memilih versi mobile pada render awal, struktur node berbeda total.
Pola yang lebih aman:
- Utamakan CSS responsif jika hanya masalah tampilan.
- Jika benar-benar perlu perbedaan logika, render versi netral dahulu lalu evaluasi di klien setelah mount.
- Untuk komponen sangat bergantung pada API browser, gunakan client-only render.
Contoh reproduksi singkat: dari gejala ke akar masalah
Berikut contoh kecil yang tampak sepele tetapi realistis:
function UserGreeting() {
const name = typeof window === 'undefined'
? 'Guest'
: localStorage.getItem('name') || 'Guest'
return <h2>Halo, {name}</h2>
}Gejalanya:
- HTML dari server menampilkan
Halo, Guest. - Setelah load, teks berubah menjadi
Halo, Budi. - Framework bisa menampilkan peringatan hydration mismatch.
Akar masalahnya: render server dan render awal klien memakai sumber data berbeda.
Perbaikan minimal:
function UserGreeting() {
const [name, setName] = useState('Guest')
useEffect(() => {
const saved = localStorage.getItem('name')
if (saved) setName(saved)
}, [])
return <h2>Halo, {name}</h2>
}Perbaikan yang lebih baik jika nama penting untuk tampilan awal: simpan nama di cookie atau session yang bisa dibaca server, lalu kirim sebagai props agar SSR dan klien sama sejak awal.
Checklist diagnosis hydration mismatch
Saat bug muncul, gunakan checklist berikut agar investigasi tidak melebar ke mana-mana.
Checklist cepat
- Bandingkan HTML dari server dengan DOM setelah hydration.
- Cek warning di console yang menyebut teks, atribut, atau struktur node yang berbeda.
- Audit render path untuk
Date,Math.random, UUID acak, dan formatter locale. - Cari akses
window,document,navigator,localStorage, atausessionStoragesaat render. - Periksa apakah ada conditional render berbasis viewport, media query JS, atau feature detection browser.
- Pastikan data SSR dipakai sebagai state awal di klien.
- Lihat apakah fetch ulang saat mount mengubah data terlalu cepat.
- Periksa key list dan ID elemen: apakah stabil antar render?
Checklist investigasi lebih dalam
- Reproduksi di mode development agar warning lebih jelas.
- Capture output server: lihat HTML yang benar-benar dikirim, bukan hanya hasil akhir di inspector.
- Log input render di server dan klien: props, locale, timezone, flag environment, dan data awal.
- Bekukan nilai nondeterministik satu per satu, misalnya ganti waktu dengan konstanta, lalu lihat apakah mismatch hilang.
- Nonaktifkan fetch klien sementara untuk memisahkan masalah data awal dari revalidasi.
- Kurangi komponen sampai tersisa unit terkecil yang masih memicu mismatch.
Debugging yang baik biasanya bukan menambah lebih banyak log secara acak, tetapi memperkecil ruang kemungkinan sampai penyebabnya terlihat jelas.
Langkah reproduksi yang praktis
Kalau bug belum konsisten, buat reproduksi yang dapat diulang. Ini penting agar Anda bisa membuktikan perbaikan, bukan sekadar berharap bug hilang.
- Buat halaman kecil yang hanya merender komponen bermasalah.
- Hilangkan dependency lain yang tidak relevan.
- Gunakan input tetap untuk props dan data server.
- Simulasikan environment berbeda, misalnya timezone server vs browser, atau localStorage berisi nilai tertentu.
- Reload halaman penuh, jangan hanya navigasi client-side.
- Bandingkan output sebelum dan sesudah hydration.
Jika penyebabnya adalah locale/timezone, reproduksi sering baru terlihat saat:
- server container berjalan di UTC,
- browser user di timezone lokal,
- format tanggal dibuat langsung saat render.
Jika penyebabnya localStorage, reproduksi baru terlihat saat browser sudah memiliki nilai tersimpan yang berbeda dari fallback server.
Strategi perbaikan yang paling sering efektif
1. Samakan input awal server dan klien
Ini strategi terbaik bila memungkinkan. Jika data dibutuhkan untuk render awal, usahakan server dan klien memakai nilai yang sama melalui props serialized, cookie, session, atau state hydration payload.
Cocok untuk: data user, preferensi tema, hasil fetch awal, flag fitur, locale yang sudah diketahui.
2. Tunda pembacaan API browser ke fase setelah mount
Gunakan efek atau lifecycle setara untuk membaca window, localStorage, ukuran viewport, atau API browser lainnya setelah hydration selesai.
Cocok untuk: data yang memang hanya tersedia di browser dan tidak kritikal untuk HTML awal.
Trade-off: bisa ada perubahan UI sesaat. Kurangi dengan placeholder yang netral atau skeleton yang tidak menipu user.
3. Hindari nilai acak dan waktu langsung di render path
Jika Anda butuh timestamp, ID, atau token visual, buat sekali di server atau gunakan nilai stabil dari model data. Jangan menghitung ulang secara bebas di render awal klien.
4. Bedakan antara SSR data dan client revalidation
Jika klien perlu memverifikasi data terbaru, lakukan sebagai langkah berikutnya, bukan mengganti sumber data awal secara diam-diam pada render pertama.
Pola yang sehat:
- render dari data SSR,
- hydrate dengan state awal yang sama,
- lalu revalidate jika perlu, dengan indikator loading atau transisi yang wajar.
5. Gunakan CSS untuk responsivitas, bukan JS saat render awal
Jika perbedaannya hanya tampilan mobile vs desktop, lebih aman menyerahkan ke CSS. Semakin sedikit keputusan berbasis browser saat render awal, semakin kecil peluang hydration mismatch.
Kapan sebaiknya memakai client-only render?
Tidak semua komponen layak dipaksa SSR. Kadang pilihan paling tepat adalah menjadikannya client-only, terutama jika komponen sangat bergantung pada API browser atau nilainya memang personal dan berubah cepat.
Pertimbangkan client-only render jika:
- komponen bergantung penuh pada
window, canvas, editor rich text, peta, atau library DOM-heavy, - nilai awal tidak bisa disamakan secara praktis di server,
- SEO tidak penting untuk bagian tersebut,
- konten awalnya bukan elemen kritikal above the fold.
Namun ada trade-off:
- HTML awal untuk komponen itu kosong atau placeholder.
- Waktu interaktif bisa terasa lebih lambat.
- SEO dan performa perceived load bisa menurun jika dipakai berlebihan.
Jadi, client-only render adalah alat yang valid, tetapi sebaiknya dipakai karena alasan teknis yang jelas, bukan sebagai pelarian dari debugging.
Kesalahan umum saat memperbaiki hydration mismatch
- Hanya membungkam warning tanpa menyamakan output render awal.
- Menambah guard
typeof window !== 'undefined'tetapi tetap menghasilkan UI awal berbeda. - Mengandalkan efek samping untuk data penting padahal data itu dibutuhkan sejak SSR.
- Mengabaikan locale/timezone karena kode terlihat benar di mesin lokal.
- Menyalahkan framework terlalu cepat padahal penyebabnya input render yang tidak stabil.
Pola mental yang membantu bug cepat selesai
Saat menghadapi hydration mismatch, jangan mulai dari “framework ini aneh”. Mulailah dari model yang lebih sederhana:
Untuk input awal yang sama, server dan klien harus menghasilkan markup awal yang sama.
Kalau kenyataannya berbeda, berarti ada input yang tidak sama, tidak stabil, atau hanya tersedia di salah satu environment. Dengan cara pandang ini, investigasi menjadi jauh lebih terarah. Anda tidak sedang mengejar gejala UI acak; Anda sedang mencari sumber ketidaksamaan render.
Penutup
Memahami akar hydration mismatch berarti memahami kontrak dasar SSR: HTML server dan render awal klien harus konsisten. Gejala seperti teks berubah setelah load, event handler tidak aktif, layout meloncat, atau state klien berbeda hampir selalu berasal dari pelanggaran kontrak ini.
Dalam praktiknya, penyebab paling umum adalah data nondeterministik, akses API browser terlalu dini, perbedaan locale/timezone, ID acak, race condition fetch, dan conditional render berbasis environment klien. Jika Anda membiasakan diri membandingkan output server dan render awal klien secara disiplin, bug SSR yang tadinya terasa misterius biasanya berubah menjadi masalah yang cukup mekanis untuk diperbaiki.
Komentar
0 komentar
Masuk ke akun kamu untuk ikut berkomentar.
Belum ada komentar
Jadilah yang pertama ikut berdiskusi!