Hydration mismatch pada SSR modern muncul ketika markup HTML yang dikirim server berbeda dengan hasil render pertama di browser. Gejalanya bisa berupa warning di console, UI berkedip, event handler tidak terpasang seperti yang diharapkan, state awal meloncat, atau komponen tertentu dirender ulang sepenuhnya.

Masalah ini sering bukan bug tunggal, melainkan gabungan beberapa sumber perbedaan: akses API browser saat render awal, nilai acak atau timestamp, perbedaan locale dan timezone, auth state yang belum sinkron, conditional render yang berubah di client, atau data async yang datang terlambat. Saat men-debug, penting juga menjaga praktik aman: jangan menyuntikkan data mentah ke window, jangan mem-bypass escaping, dan selalu validasi data bootstrap agar perbaikan mismatch tidak berubah menjadi celah XSS atau data integrity issue.

Apa itu hydration mismatch dan kenapa berbahaya?

Pada SSR, server mengirim HTML awal agar halaman cepat tampil dan dapat diindeks. Setelah itu, JavaScript di browser melakukan hydration: framework mengaitkan event, state, dan perilaku interaktif ke markup yang sudah ada. Jika struktur atau isi HTML versi server tidak sama dengan hasil render awal di client, framework biasanya memberi peringatan atau membuang subtree tertentu lalu merender ulang di browser.

Dampaknya tidak selalu hanya visual. Hydration mismatch bisa menyebabkan:

  • UI flicker: teks, layout, atau tombol berubah sesaat setelah halaman tampil.
  • State awal tidak konsisten: komponen menganggap nilai awal berbeda antara server dan browser.
  • Masalah interaktivitas: event handler tidak terpasang pada node yang diharapkan.
  • Debug menjadi menyesatkan: akar masalah sering ada di data, locale, auth, atau urutan render, bukan di komponen yang terlihat rusak.

Checklist diagnosis hydration mismatch

Gunakan checklist berikut secara berurutan. Tujuannya bukan hanya menemukan gejala, tetapi memastikan sumber perbedaan antara render server dan render awal client benar-benar terisolasi.

1. Pastikan perbedaan terjadi pada render awal, bukan setelah efek berjalan

Pisahkan antara mismatch saat hydration dan update state setelah komponen mounted. Jika perubahan baru muncul setelah useEffect, watcher, atau hook lifecycle lain berjalan, itu mungkin bukan mismatch, melainkan transisi state normal.

  • Periksa warning hydration di console browser.
  • Bandingkan HTML respons server dengan DOM segera setelah load, sebelum interaksi.
  • Matikan sementara efek samping non-kritis untuk memastikan sumbernya.

2. Cari penggunaan API browser saat render awal

Sumber paling umum adalah penggunaan window, document, localStorage, sessionStorage, matchMedia, ukuran viewport, atau cookie browser langsung di fungsi render. Server tidak memiliki kondisi runtime yang sama dengan browser.

Pola salah:

function ThemeLabel() {
  const theme = localStorage.getItem('theme') || 'light';
  return <span>{theme}</span>;
}

Masalahnya: server tidak bisa membaca localStorage, sehingga hasil render server dan client awal bisa berbeda.

Pola lebih aman:

function ThemeLabel() {
  const [theme, setTheme] = useState('light');

  useEffect(() => {
    const saved = window.localStorage.getItem('theme');
    if (saved) setTheme(saved);
  }, []);

  return <span>{theme}</span>;
}

Kenapa ini bekerja: server dan client sama-sama merender nilai awal light, lalu browser melakukan update setelah komponen siap. Trade-off-nya adalah ada kemungkinan perubahan tampilan singkat setelah mount. Jika itu masalah, gunakan strategi bootstrap state yang tervalidasi dari server.

3. Audit nilai yang tidak deterministik

Nilai seperti Date.now(), new Date(), Math.random(), UUID yang dibuat saat render, atau ID berbasis waktu adalah penyebab klasik mismatch.

Pola salah:

function BuildStamp() {
  return <small>{Date.now()}</small>;
}

Pola benar: hitung nilainya di server lalu kirim sebagai data, atau tunda pembuatan nilai sampai setelah mount jika memang hanya dibutuhkan di client.

function BuildStamp({ renderedAt }) {
  return <small>{renderedAt}</small>;
}

Jika Anda perlu ID stabil untuk atribut aksesibilitas atau korelasi DOM, gunakan mekanisme ID yang memang dirancang stabil oleh framework, bukan generator acak di dalam render.

4. Periksa locale, timezone, dan format numerik

Format tanggal, mata uang, pemisah ribuan, dan bahasa dapat berbeda antara lingkungan server dan browser. Bahkan bila datanya sama, hasil string yang dirender bisa berbeda.

  • Jangan mengandalkan locale default.
  • Tentukan locale secara eksplisit bila output harus identik.
  • Waspadai timezone server yang berbeda dengan user.

Pola berisiko:

function Price({ amount }) {
  return <span>{amount.toLocaleString()}</span>;
}

Pola lebih aman:

function Price({ amount, locale }) {
  return <span>{amount.toLocaleString(locale)}</span>;
}

Jika timezone user memang harus dipakai, pertimbangkan render placeholder netral di server lalu format final setelah mount. Trade-off-nya adalah ada perubahan tampilan awal, tetapi terkontrol dan tidak menimbulkan mismatch.

5. Validasi auth state dan user context

UI yang bergantung pada status login sering mismatch ketika server menganggap user anonim, tetapi browser segera membaca token atau session lokal lalu mengubah tampilan menjadi user login.

Contoh gejala:

  • Tombol Masuk di server berubah menjadi avatar user di client.
  • Menu role-based tampil berbeda sesaat setelah load.
  • Komponen premium muncul setelah hydration meski HTML awal tidak memuatnya.

Pendekatan yang lebih aman:

  • Gunakan sumber auth yang sama untuk server dan client bila memungkinkan, misalnya session yang tervalidasi di server.
  • Jika auth hanya bisa diketahui di browser, render state netral terlebih dahulu.
  • Hindari conditional render yang tajam pada render pertama bila data auth belum siap.

6. Telusuri conditional render yang bergantung lingkungan

Cabang UI seperti if (typeof window !== 'undefined') langsung di render path sering memicu struktur DOM berbeda. Masalahnya bukan hanya isi teks, tetapi bisa berupa jumlah node, urutan elemen, atau atribut yang berubah.

Pola salah:

function Banner() {
  if (typeof window !== 'undefined') {
    return <div>Promo client</div>;
  }
  return <div>Promo server</div>;
}

Pola lebih aman: render markup yang sama lebih dulu, lalu ubah perilaku atau isi sekunder setelah mount.

7. Cek data async yang terlambat atau tidak identik

Pada SSR modern, mismatch sering terjadi saat server merender dengan snapshot data A, sementara client memulai dengan data kosong, fallback berbeda, atau mem-fetch ulang dan menerima data B sebelum hydration selesai.

  • Pastikan payload data awal benar-benar sama dengan yang dipakai client.
  • Jangan mengganti struktur list dari kosong menjadi berisi item pada render pertama tanpa strategi yang konsisten.
  • Periksa race condition antara bootstrap data dan fetch ulang di browser.

Jika ada cache di sisi client, pastikan kunci dan isi cache selaras dengan data SSR, bukan sekadar mirip.

Pola salah vs benar yang paling sering ditemui

Akses cookie atau storage di render path

Salah: menentukan varian UI langsung dari storage browser saat render.

Benar: kirim state awal dari server atau tunda pembacaan ke fase client-only.

Menghasilkan key list dari nilai acak

Salah:

items.map(item => <li key={Math.random()}>{item.name}</li>)

Ini membuat identitas node tidak stabil, mempersulit reconciliation, dan bisa memperburuk mismatch.

Benar: gunakan identifier data yang stabil, misalnya ID dari backend.

Formatting tanggal langsung dari lingkungan lokal

Salah: format bergantung timezone default runtime.

Benar: tentukan locale/timezone eksplisit atau render placeholder netral sampai data client tersedia.

Bootstrap state mentah ke global window

Salah:

<script>
  window.__BOOTSTRAP__ = { userName: '<%= userInput %>' };
</script>

Jika data tidak di-escape dengan benar, ini berisiko XSS. Selain itu, data yang bentuknya tidak tervalidasi dapat membuat client merender state berbeda dari yang diasumsikan server.

Lebih aman:

  • Serialisasi data dengan encoder yang aman untuk konteks HTML/JS.
  • Validasi shape data bootstrap sebelum dipakai.
  • Batasi hanya field yang dibutuhkan untuk hydration.

Sebagai pengingat sejalan dengan praktik keamanan web yang umum dirujuk di MDN: hindari menyisipkan string mentah ke HTML atau script, jangan menonaktifkan escaping demi “mempercepat debug”, dan perlakukan data bootstrap sebagai input yang harus divalidasi.

Langkah isolasi bug yang efektif

1. Reproduksi dengan input sekecil mungkin

Buat halaman atau route minimal yang hanya memuat komponen bermasalah. Tujuannya mengurangi gangguan dari provider global, analytics, eksperimen A/B, atau script pihak ketiga.

2. Bandingkan sumber HTML server dengan render client awal

Lihat HTML respons server apa adanya, bukan DOM setelah browser memodifikasi halaman. Cari perbedaan pada:

  • isi teks,
  • urutan elemen,
  • atribut seperti class, checked, selected,
  • jumlah item list,
  • node yang hanya muncul di salah satu sisi.

3. Bekukan sumber nondeterministik

Saat investigasi, buat semua faktor non-stabil menjadi tetap:

  • pakai timestamp tetap,
  • pakai locale eksplisit,
  • matikan fitur eksperimen,
  • nonaktifkan fetch ulang otomatis sesaat setelah mount.

Jika mismatch hilang, Anda sudah mempersempit akar masalah.

4. Tambahkan logging terarah, bukan logging berlebihan

Log nilai yang benar-benar memengaruhi output render awal: locale, timezone, auth marker, panjang list, dan nilai props penting. Bedakan log server dan browser dengan prefix yang jelas.

// pseudo-logging
console.log('[server] locale=', locale, 'items=', items.length)
console.log('[client] locale=', locale, 'items=', items.length)

Jangan log data sensitif seperti token, cookie mentah, atau payload user lengkap ke console hanya demi debug.

5. Nonaktifkan cabang rendering satu per satu

Jika komponen besar sulit dianalisis, sementara ganti cabang dinamis dengan output statis. Misalnya, ganti formatter tanggal dengan string tetap, atau paksa auth state menjadi netral. Pendekatan ini membantu menemukan cabang yang membuat markup berubah.

6. Periksa integrasi pihak ketiga

Widget analytics, personalisasi, iklan, atau script eksternal kadang memodifikasi DOM sebelum hydration selesai. Jika mismatch hanya muncul pada halaman tertentu, audit script yang berjalan lebih awal dari framework Anda.

Guardrail keamanan saat debug hydration mismatch

Perbaikan cepat sering menggoda, tetapi beberapa “solusi” justru membuka risiko keamanan. Berikut guardrail yang layak dijadikan standar tim.

Jangan menyuntikkan data mentah ke window atau script inline

Menaruh objek mentah dari server ke window.__INITIAL_STATE__ tanpa serialisasi aman dapat memicu XSS jika ada karakter khusus atau input user. Solusi yang benar adalah serialisasi yang aman untuk konteks script, plus validasi saat dibaca di client.

Jangan mem-bypass escaping untuk menyamakan output

Jika string server dan client berbeda karena encoding atau formatting, jangan langsung memakai HTML mentah agar “hasilnya sama”. Menonaktifkan escaping atau memakai injeksi HTML tanpa sanitasi biasanya menyelesaikan gejala sambil menciptakan celah yang lebih serius.

Validasi data bootstrap sebelum hydration

Data awal yang dikirim server harus dianggap sebagai kontrak. Validasi shape minimal: tipe field, nullability, enum sederhana, dan keberadaan properti penting. Ini membantu dua hal sekaligus: mencegah mismatch karena data tak terduga dan mengurangi risiko data berbahaya masuk ke jalur render.

Jangan mengekspos token atau detail session di payload debug

Untuk membandingkan state server dan client, cukup log penanda non-sensitif, misalnya isAuthenticated: true atau userRole: 'admin' jika memang perlu. Hindari menulis token, email lengkap, atau isi cookie ke DOM, console, atau global scope.

Checklist code review untuk mencegah mismatch terulang

  1. Apakah render awal mengakses API browser seperti window, document, atau storage?
  2. Apakah ada penggunaan Date.now(), new Date(), Math.random(), atau generator ID acak di render?
  3. Apakah formatting tanggal/angka bergantung locale atau timezone default?
  4. Apakah auth state server dan client berasal dari sumber yang sama atau setidaknya memiliki fallback netral?
  5. Apakah conditional render mengubah struktur DOM antara server dan client?
  6. Apakah data async awal identik dengan yang dipakai client untuk hydration?
  7. Apakah key pada list stabil dan berasal dari data, bukan dari indeks yang berubah atau nilai acak?
  8. Apakah data bootstrap diserialisasi dengan aman dan divalidasi sebelum dipakai?
  9. Apakah ada script pihak ketiga yang memodifikasi DOM sebelum hydration selesai?
  10. Apakah log/debug output menghindari data sensitif dan tidak menambahkan celah XSS?

Kapan memakai placeholder, kapan memaksakan kesamaan output?

Tidak semua perbedaan harus dipaksa identik sejak byte pertama. Pilihan terbaik bergantung pada sifat datanya:

  • Harus identik saat render awal: teks struktural, jumlah item utama, atribut penting, state form awal, key list, label aksesibilitas.
  • Boleh diperbarui setelah mount: preferensi lokal browser, timezone user, ukuran viewport, tema dari storage, data non-kritis yang memang client-specific.

Prinsipnya: jika informasi hanya valid di browser, lebih aman tampilkan fallback yang konsisten lalu update setelah mount. Jika informasi seharusnya sudah diketahui server, pastikan server dan client membaca sumber data yang sama.

Penutup

Checklist aman debug hydration mismatch pada SSR modern pada dasarnya adalah disiplin untuk menjaga render awal tetap deterministik. Mulailah dari hal paling umum: API browser di render, nilai acak, locale/timezone, auth state, conditional render, dan data async yang tidak sinkron. Setelah itu, isolasi bug secara sistematis dengan membandingkan HTML server dan render client awal.

Yang tidak kalah penting, jangan biarkan proses debug merusak postur keamanan aplikasi. Hindari injeksi data mentah ke global scope, jangan mem-bypass escaping, dan validasi data bootstrap sebelum hydration. Dengan pola ini, Anda bukan hanya menghilangkan warning, tetapi juga membuat SSR lebih stabil, dapat diprediksi, dan aman untuk dipelihara dalam jangka panjang.