Hydration mismatch terjadi ketika HTML hasil server-side rendering (SSR) tidak sama dengan hasil render pertama di browser. Gejalanya sering terasa sepele—warning di console, UI berkedip, teks berubah sendiri—tetapi akar masalahnya biasanya adalah hal-hal yang hanya diketahui browser: localStorage, ukuran layar, waktu lokal, nilai acak, atau kondisi berbasis window/document.
Kalau Anda lelah “berdebat dengan tool” dan menebak-nebak penyebabnya, pendekatan terbaik adalah debug yang sistematis: identifikasi bagian output yang berbeda, isolasi komponen, lalu pindahkan state yang hanya valid di client agar tidak memengaruhi render awal SSR. Fokus artikel ini adalah cara diagnosis dan perbaikan yang praktis.
Apa yang Sebenarnya Terjadi Saat Hydration?
Pada SSR, server menghasilkan HTML awal agar halaman bisa tampil cepat. Setelah itu, JavaScript di browser melakukan hydration: framework memasang event listener dan menyambungkan komponen ke HTML yang sudah ada. Proses ini mengasumsikan bahwa render pertama di client menghasilkan markup yang sama dengan yang sudah dikirim server.
Masalah muncul ketika render awal di browser memakai informasi yang tidak tersedia atau berbeda saat SSR. Akibatnya, framework mendeteksi perbedaan antara DOM dari server dan hasil render client.
Intinya: hydration mismatch bukan sekadar error UI, tetapi tanda bahwa asumsi “SSR dan render pertama client harus identik” telah dilanggar.
Gejala Umum Hydration Mismatch
Warning seperti Text content does not match server-rendered HTML.
Elemen yang semula tampil lalu berubah sesaat setelah halaman aktif.
Class CSS atau atribut berbeda setelah hydration.
Input kehilangan nilai awal atau berpindah mode controlled/uncontrolled.
Komponen tertentu error hanya saat SSR aktif, tetapi normal di SPA murni.
Di Next.js, Nuxt, atau setup SSR lain, gejalanya mirip walau pesan error bisa sedikit berbeda. Framework-nya berbeda, tetapi pola sumber masalahnya hampir selalu sama.
Akar Masalah yang Paling Sering
1. State dari localStorage atau sessionStorage
Server tidak punya akses ke localStorage. Jika render awal membaca nilai dari sana untuk menentukan teks, tema, tab aktif, atau isi keranjang, maka output client bisa berbeda dari SSR.
2. Waktu, tanggal, locale, dan timezone
new Date(), format tanggal lokal, atau perhitungan “berapa menit lalu” dapat menghasilkan output berbeda antara server dan browser. Perbedaan timezone sangat sering memicu mismatch.
3. Nilai acak atau ID non-deterministik
Pemakaian Math.random(), generator ID acak, atau nilai yang berubah di setiap render dapat membuat markup awal tidak stabil.
4. Media query dan ukuran layar
Logika seperti “kalau mobile tampilkan ini, kalau desktop tampilkan itu” sering dibangun dari window.innerWidth atau matchMedia. Server tidak tahu ukuran viewport browser pengguna.
5. Kondisi berbasis window, document, navigator
Pemeriksaan seperti window !== undefined memang mencegah crash, tetapi belum tentu mencegah mismatch. Jika hasil render berubah karena kondisi itu, HTML server dan client tetap bisa berbeda.
6. Efek samping saat render
Render seharusnya deterministik. Jika ada mutasi global, pembacaan environment yang berubah, atau transformasi data yang tidak stabil saat render, mismatch akan lebih sulit dilacak.
Contoh Sebelum dan Sesudah
Contoh 1: localStorage untuk tema
Sebelum — render awal bergantung pada state lokal browser:
function ThemeLabel() {
const [theme] = useState(() => {
return localStorage.getItem('theme') || 'light';
});
return <span>Tema: {theme}</span>;
}Masalahnya: server tidak bisa membaca localStorage, sehingga SSR mungkin menghasilkan Tema: light, tetapi client langsung merender Tema: dark.
Sesudah — gunakan deferred client state:
function ThemeLabel() {
const [theme, setTheme] = useState(null);
useEffect(() => {
setTheme(localStorage.getItem('theme') || 'light');
}, []);
return <span>Tema: {theme ?? '...'}</span>;
}Pendekatan ini bekerja karena SSR dan render pertama client sama-sama menampilkan placeholder yang stabil, lalu state lokal diterapkan setelah hydration.
Contoh 2: waktu relatif
Sebelum — menghitung waktu relatif saat render:
function LastUpdated({ publishedAt }) {
const minutes = Math.floor((Date.now() - new Date(publishedAt).getTime()) / 60000);
return <p>Diperbarui {minutes} menit lalu</p>;
}Selisih beberapa detik saja antara server dan client dapat mengubah output.
Sesudah — kirim nilai stabil dari server, atau render format statis dulu:
function LastUpdated({ publishedAt }) {
return <p>Diperbarui pada {publishedAt}</p>;
}Jika memang perlu format relatif, hitung setelah mount di client, atau kirim string final yang sudah dihitung dari server.
Contoh 3: media query
Sebelum — render berdasarkan lebar layar saat inisialisasi:
function Sidebar() {
const [isMobile] = useState(() => window.innerWidth < 768);
return isMobile ? <MobileNav /> : <DesktopSidebar />;
}Sesudah — prioritaskan CSS untuk layout responsif, atau tunda logika client-only:
function SidebarShell() {
const [isMobile, setIsMobile] = useState(null);
useEffect(() => {
const media = window.matchMedia('(max-width: 767px)');
const update = () => setIsMobile(media.matches);
update();
media.addEventListener?.('change', update);
return () => media.removeEventListener?.('change', update);
}, []);
if (isMobile === null) {
return <nav className="sidebar-placeholder" />;
}
return isMobile ? <MobileNav /> : <DesktopSidebar />;
}Kalau perbedaan hanya visual, lebih aman biarkan CSS yang menangani responsivitas tanpa mengubah struktur HTML saat hydration.
Checklist Investigasi: Jangan Tebak, Verifikasi
Saat warning hydration muncul, gunakan checklist ini untuk mempersempit sumber masalah.
Temukan komponen pertama yang berbeda. Jangan mulai dari root layout. Lacak hingga node atau teks yang benar-benar berubah.
Bandingkan output server dan render pertama client. Fokus pada teks, atribut, class, urutan elemen, dan kondisi bercabang.
Cari akses ke API browser. Periksa penggunaan
window,document,localStorage,sessionStorage,navigator,matchMedia.Cari nilai non-deterministik. Misalnya
Date.now(),new Date(),Math.random(), ID acak, locale default.Periksa branch render. Apakah ada
ifyang menghasilkan struktur DOM berbeda antara server dan client?Audit inisialisasi state. Lazy initializer di
useStateatau state awal di komponen sering menjadi sumber mismatch.Uji dengan data statis. Ganti sumber data dinamis sementara untuk memastikan masalah memang berasal dari state lokal, bukan fetch atau transformasi data.
Isolasi komponen. Komentari subtree tertentu atau render placeholder sederhana untuk mengetahui area yang memicu warning.
Strategi Isolasi Komponen
Salah satu kesalahan paling umum adalah memperbaiki banyak hal sekaligus. Akibatnya, penyebab aslinya tidak pernah benar-benar dipahami. Lebih efektif gunakan strategi isolasi berikut:
1. Bekukan output
Ganti komponen yang dicurigai dengan markup statis:
function ProblematicWidget() {
return <div>static</div>;
}Jika warning hilang, Anda tahu mismatch ada di dalam widget tersebut, bukan di parent-nya.
2. Kembalikan fitur satu per satu
Tambahkan lagi logika localStorage, media query, atau format waktu secara bertahap. Cara ini lebih cepat daripada menebak satu baris dari ratusan baris render.
3. Pisahkan sumber state
Tanyakan: state ini berasal dari mana?
Server-known: cookie, parameter route, data dari backend.
Client-only: localStorage, viewport, preferensi browser, DOM state.
Jika state termasuk client-only, jangan biarkan ia menentukan SSR markup tanpa mekanisme sinkronisasi yang aman.
4. Buat fallback yang stabil
Alih-alih memaksa nilai “benar” muncul sejak SSR, buat output awal yang netral dan konsisten. Setelah hydration selesai, baru perbarui state.
Pola Aman untuk Mencegah Hydration Mismatch
Deferred client state
Ini pola paling penting: render output awal yang aman untuk SSR, lalu isi state dari browser di useEffect, onMounted, atau lifecycle client-only lain.
Cocok untuk:
tema dari localStorage,
tab terakhir yang dibuka,
preferensi user yang hanya ada di browser,
deteksi viewport atau media query.
Placeholder yang stabil
Placeholder bukan solusi kosmetik; ia alat sinkronisasi. Kalau data client-only belum tersedia, tampilkan bentuk awal yang sama di server dan client. Contohnya:
...untuk label sederhana,skeleton loader ringan,
container kosong dengan tinggi tetap agar layout tidak meloncat.
Trade-off-nya adalah ada sedikit penundaan sebelum nilai final muncul. Namun ini lebih baik daripada mismatch dan UI yang tidak konsisten.
Guard di useEffect atau onMounted
Jika kode harus memakai API browser, jalankan di fase client setelah mount.
useEffect(() => {
const raw = window.localStorage.getItem('cart');
// parse dan set state di sini
}, []);Pola serupa berlaku di framework lain dengan hook lifecycle client-side.
Utamakan CSS untuk responsivitas
Kalau perbedaan hanya presentasi, jangan pakai branch render berbasis viewport. Gunakan CSS breakpoint agar struktur DOM tetap sama antara server dan client.
Gunakan input stabil dari server
Untuk waktu, locale, atau personalisasi awal, lebih aman mengirim nilai yang sudah ditentukan server dibanding menghitung ulang saat render pertama di client. Misalnya string tanggal yang sudah diformat, atau preferensi tema dari cookie yang juga bisa dibaca server.
Kesalahan Umum yang Sering Menjebak
“Saya sudah cek typeof window, jadi aman”
Tidak selalu. Guard ini mencegah runtime error di server, tetapi kalau hasil render berubah berdasarkan ada/tidaknya window, mismatch tetap bisa terjadi.
Mencampur SSR state dan client-only state dalam satu render path
Contoh klasik: data produk datang dari server, tetapi urutannya diubah berdasarkan preferensi lokal browser saat render awal. Hasilnya, SSR HTML dan client HTML tidak sama.
Menghasilkan key atau ID secara acak
Key acak saat render membuat rekonsiliasi tidak stabil. Jika butuh ID, gunakan sumber yang deterministik dari data atau hasilkan di fase yang tepat.
Menganggap warning bisa diabaikan
Beberapa mismatch memang tampak “tetap jalan”, tetapi efek sampingnya bisa merusak event binding, memicu render ulang tak perlu, atau menghasilkan flicker yang sulit direproduksi.
Pendekatan Praktis di Next.js, Nuxt, dan Inertia
Walau implementasi tiap framework berbeda, prinsip debug-nya sama:
Next.js: curigai komponen yang merender berbeda antara server dan browser, terutama yang membaca localStorage atau window saat inisialisasi state.
Nuxt: pastikan data client-only dipindahkan ke lifecycle client, dan hati-hati dengan cabang render berbasis browser API.
Inertia: walau modelnya berbeda dari SSR framework murni, mismatch tetap bisa muncul jika halaman pertama atau komponen tertentu mencoba merender state browser-only sebelum siap.
Jangan terpaku pada framework-specific workaround lebih dulu. Mulai dari pertanyaan dasar: apakah server dan client punya input render yang sama? Kalau tidak, mismatch hanya soal waktu.
Langkah Debug yang Bisa Langsung Dipakai
Baca warning dan identifikasi elemen atau teks yang dilaporkan berbeda.
Cari komponen yang merender elemen tersebut.
Audit semua state awal dan conditional rendering di komponen itu.
Hapus sementara pembacaan localStorage, waktu, random, media query, atau akses window/document.
Jika warning hilang, kembalikan satu per satu sampai penyebab tepatnya ditemukan.
Pindahkan logika client-only ke
useEffect/onMountedatau gunakan placeholder stabil.Verifikasi bahwa SSR output dan render pertama client kini identik.
Kalau Anda merasa sedang “berdebat dengan tool”, biasanya masalahnya bukan tool yang rewel. Tool hanya menunjukkan bahwa render Anda tidak deterministik antara server dan browser. Begitu diagnosis dilakukan per sumber state, masalahnya jauh lebih mekanis dan mudah diperbaiki.
Penutup
Debug hydration mismatch akibat state lokal dan SSR sebaiknya tidak dilakukan dengan trial-and-error. Fokuslah pada perbedaan input render antara server dan browser: localStorage, waktu, random value, media query, serta kondisi berbasis window atau document. Setelah sumbernya jelas, gunakan pola aman seperti deferred client state, placeholder stabil, dan guard di lifecycle client-only.
Tujuannya bukan membuat warning hilang dengan cara cepat, tetapi memastikan SSR tetap deterministik. Begitu prinsip ini dipegang, mismatch yang tadinya terasa melelahkan biasanya berubah menjadi bug yang bisa dilokalisasi dan diselesaikan dengan rapi.
Komentar
0 komentar
Masuk ke akun kamu untuk ikut berkomentar.
Belum ada komentar
Jadilah yang pertama ikut berdiskusi!