Checklist debug hydration SSR dibutuhkan saat UI server-rendered terlihat benar, tetapi berubah sendiri begitu JavaScript klien aktif. Masalah ini sering muncul karena hasil render awal di server berbeda dengan hasil render awal di browser, lalu proses hydration gagal mencocokkan DOM yang sudah ada.
Intinya sederhana: server render harus menghasilkan markup yang konsisten dengan client hydrate pada render pertama. Jika Anda memakai localStorage, Date.now(), Math.random(), window, media query, atau data async secara ceroboh saat initial render, UI hampir pasti berisiko mismatch. Artikel ini fokus pada gejala, root cause, urutan investigasi, dan langkah perbaikan yang bisa langsung dipakai tim frontend.
Memahami beda server render vs client hydrate
Pada aplikasi SSR modern seperti React dengan Next.js atau framework serupa, ada dua fase penting:
- Server render: server menghasilkan HTML awal dari komponen.
- Client hydrate: browser memuat JavaScript lalu React "menempel" ke HTML yang sudah ada, menghubungkan event handler dan state.
Hydration berjalan baik hanya jika output render pertama di browser sama dengan HTML yang dibuat server. Jika tidak sama, Anda bisa melihat:
- Warning seperti Text content does not match server-rendered HTML.
- UI berkedip atau berubah setelah load.
- Class CSS, atribut, atau struktur DOM berbeda.
- State awal terasa "lompat" setelah komponen aktif.
- Event handler terpasang pada elemen yang tidak sesuai.
Kesalahan umum adalah mengira hydration mismatch selalu soal framework. Lebih sering penyebabnya adalah logika render yang tidak deterministik antara server dan browser.
Gejala paling umum saat UI klien berubah sendiri
1. Teks awal berubah setelah halaman tampil
Contoh: server menampilkan Selamat pagi, tetapi setelah hydrate berubah menjadi Selamat malam karena komponen membaca waktu lokal pengguna saat render pertama.
2. Tema gelap/terang berkedip
Server merender mode default terang, lalu klien membaca localStorage atau preferensi sistem dan langsung mengganti ke mode gelap. Ini sering terlihat sebagai flash atau mismatch class.
3. Layout berbeda antara server dan browser
Komponen merender versi mobile atau desktop berdasarkan window.matchMedia saat initial render. Server tidak punya informasi browser yang sama, sehingga HTML awal berbeda.
4. Daftar item atau ID berubah
Komponen memakai nilai acak atau timestamp sebagai key, label, atau ID. Server dan browser menghasilkan nilai berbeda, lalu React gagal mencocokkan node yang benar.
5. Data async tampak ganda atau berganti mendadak
Server merender dari satu snapshot data, klien melakukan fetch ulang terlalu cepat dan langsung merender hasil berbeda sebelum hydration stabil.
Root cause yang paling sering memicu hydration mismatch
Render yang tidak deterministik
Semua yang menghasilkan output berbeda antara server dan klien saat render pertama adalah kandidat utama. Beberapa contoh:
Date.now()new Date()tanpa pengendalian zona waktu/formatMath.random()- pembacaan
window,document,navigator localStorage,sessionStorage- media query berbasis browser saat render
- fetch data tambahan yang mengubah output terlalu dini
Percabangan berbasis environment di dalam render
Pola seperti typeof window !== 'undefined' memang sering dipakai, tetapi jika hasilnya mengubah markup saat render pertama, mismatch tetap bisa terjadi.
function Greeting() {
if (typeof window !== 'undefined') {
return <p>Client</p>;
}
return <p>Server</p>;
}Kode di atas hampir pasti merender output berbeda antara server dan browser.
State awal diambil dari sumber yang hanya ada di browser
Ini salah satu anti-pattern yang paling sering dilakukan karena terlihat "praktis" setelah menonton satu tutorial singkat.
function ThemeToggle() {
const [theme, setTheme] = useState(() => {
return localStorage.getItem('theme') || 'light';
});
return <body data-theme={theme}>...</body>;
}Masalahnya: server tidak bisa membaca localStorage. Akibatnya, server mungkin merender light, sementara browser langsung memilih dark saat hydrate.
Key atau ID yang tidak stabil
Jika Anda membuat key dari random value atau timestamp, React kehilangan kemampuan untuk mencocokkan elemen secara konsisten.
items.map(item => <li key={Math.random()}>{item.name}</li>)Gunakan ID yang stabil dari data, bukan nilai yang berubah di setiap render.
Format lokal yang berbeda
Formatting tanggal, angka, atau locale-specific text bisa berbeda antara server dan browser jika locale, timezone, atau environment berbeda. Ini sering luput karena kode tampak "benar" secara fungsional.
Urutan investigasi: checklist debug hydration SSR yang benar
Jangan mulai dari menebak-nebak. Gunakan urutan berikut agar diagnosis lebih cepat.
1. Baca warning hydration secara literal
Framework biasanya memberi petunjuk: teks berbeda, atribut berbeda, atau struktur DOM tidak cocok. Catat node mana yang bermasalah, bukan hanya gejala visualnya.
2. Temukan output yang berubah antara HTML server dan render pertama klien
Fokus pada hal berikut:
- teks dinamis
- className dinamis
- atribut
id,data-*,style - percabangan
ifdi dalam render - urutan atau jumlah child element
Jika perlu, sederhanakan komponen sampai warning hilang. Setelah itu, aktifkan kembali bagian-bagian kode satu per satu.
3. Audit semua sumber nilai non-deterministik
Buat pencarian cepat pada codebase untuk kata kunci berikut:
Date.now(
new Date(
Math.random(
window.
document.
localStorage
sessionStorage
matchMedia
navigator.Lalu cek apakah nilai-nilai tersebut dipakai langsung dalam render atau inisialisasi state yang memengaruhi render awal.
4. Pisahkan data server dan data browser-only
Tanyakan dua hal:
- Apakah data ini harus sudah ada pada HTML awal?
- Apakah data ini hanya tersedia setelah halaman berjalan di browser?
Jika jawabannya browser-only, jangan paksa data itu menentukan markup render pertama.
5. Periksa fetch async saat mount
Jika server sudah merender data awal, klien sebaiknya memakai snapshot yang sama dulu, baru melakukan revalidasi setelah hydration. Jika klien langsung merender data lain sebelum hydration stabil, mismatch bisa terjadi.
6. Cek percabangan media query dan ukuran viewport
Jangan langsung merender struktur DOM berbeda hanya karena hasil matchMedia atau lebar layar. Lebih aman gunakan CSS untuk perbedaan layout bila memungkinkan.
7. Pastikan struktur HTML valid
Markup tidak valid atau nesting elemen yang salah dapat membuat browser memperbaiki DOM secara otomatis, lalu hasil akhirnya berbeda dari yang diharapkan React.
Contoh masalah dan perbaikannya
Kasus 1: localStorage dipakai saat inisialisasi state
Anti-pattern:
function ThemeLabel() {
const [theme] = useState(() => localStorage.getItem('theme') || 'light');
return <span>Tema: {theme}</span>;
}Masalah: server tidak punya localStorage, sehingga render awal tidak konsisten.
Perbaikan: render nilai default yang stabil, lalu sinkronkan di useEffect.
import { useEffect, useState } from 'react';
function ThemeLabel() {
const [theme, setTheme] = useState('light');
const [mounted, setMounted] = useState(false);
useEffect(() => {
const saved = window.localStorage.getItem('theme');
if (saved) setTheme(saved);
setMounted(true);
}, []);
if (!mounted) {
return <span>Tema: light</span>;
}
return <span>Tema: {theme}</span>;
}Pendekatan ini bekerja karena server dan render pertama klien sama-sama menghasilkan output yang stabil terlebih dahulu. Setelah komponen mounted, state boleh diperbarui berdasarkan browser-only API.
Trade-off: ada kemungkinan flash nilai default. Untuk tema global, solusi yang lebih baik biasanya menetapkan tema sedini mungkin di dokumen HTML atau dari cookie yang juga bisa dibaca server.
Kasus 2: waktu sekarang dirender langsung
Anti-pattern:
function Clock() {
return <p>{Date.now()}</p>;
}Perbaikan: kirim timestamp dari server sebagai props, atau render placeholder stabil lalu isi setelah mount.
function Clock({ initialTimestamp }) {
const [timestamp, setTimestamp] = useState(initialTimestamp);
useEffect(() => {
setTimestamp(Date.now());
}, []);
return <p>{timestamp}</p>;
}Jika timestamp harus mewakili waktu server, pertahankan nilai server pada render awal dan jangan menggantinya sampai setelah hydration selesai.
Kasus 3: media query menentukan struktur komponen
Anti-pattern:
function Navigation() {
const isMobile = window.matchMedia('(max-width: 768px)').matches;
return isMobile ? <MobileNav /> : <DesktopNav />;
}Masalah: server tidak tahu viewport browser sebenarnya.
Perbaikan yang lebih aman:
- Gunakan satu struktur markup yang sama, lalu bedakan tampilan dengan CSS.
- Jika harus benar-benar berbeda, tunda keputusan sampai setelah mount.
Kasus 4: random key atau ID
Anti-pattern:
function List({ items }) {
return (
<ul>
{items.map(item => (
<li key={Math.random()}>{item.name}</li>
))}
</ul>
);
}Perbaikan:
function List({ items }) {
return (
<ul>
{items.map(item => (
<li key={item.id}>{item.name}</li>
))}
</ul>
);
}Checklist praktis debug hydration mismatch
- Apakah HTML server dan render pertama klien seharusnya identik?
- Apakah ada penggunaan
Date.now(),new Date(), atauMath.random()di render? - Apakah state awal dibaca dari
localStorage,sessionStorage, atauwindow? - Apakah ada percabangan
typeof windowyang mengubah markup? - Apakah media query memengaruhi struktur DOM saat initial render?
- Apakah key list stabil dan berasal dari data nyata?
- Apakah formatting tanggal/angka bergantung locale atau timezone yang berbeda?
- Apakah fetch klien langsung mengganti data yang baru saja dirender server?
- Apakah ada ekstensi browser atau script pihak ketiga yang memodifikasi DOM sebelum hydration?
- Apakah markup HTML valid dan nesting elemen benar?
Anti-pattern yang sebaiknya dihindari tim frontend
Mengandalkan browser API dalam render
Render harus sebisa mungkin murni dan deterministik. Browser API gunakan setelah mount, atau sediakan nilainya dari server jika memang harus dipakai sejak awal.
Mencampur source of truth server dan klien tanpa kontrak yang jelas
Jika server merender satu snapshot data, klien sebaiknya mengadopsi snapshot itu lebih dulu. Jangan render dari sumber lain pada saat yang sama tanpa sinkronisasi.
Menyelesaikan mismatch dengan menyembunyikan warning saja
Menekan gejala tanpa memperbaiki penyebab biasanya memunculkan bug lain: event aneh, state lompat, atau layout tidak stabil. Warning hydration sering menandakan masalah arsitektur render, bukan sekadar noise.
Membuat UI kritis bergantung pada nilai yang belum tersedia saat SSR
Jika data hanya ada di browser, desain UI agar ada fallback stabil. Jangan memaksa SSR menebak sesuatu yang sebenarnya baru diketahui setelah mount.
Strategi pencegahan sebelum bug muncul
1. Tetapkan aturan: initial render harus deterministik
Tim perlu punya pedoman sederhana: tidak ada nilai acak, waktu real-time, atau browser-only API di render awal kecuali memang disediakan dari server.
2. Bedakan state SSR-safe dan client-only
Klasifikasikan state menjadi dua:
- SSR-safe: tersedia di server dan klien, aman untuk menentukan HTML awal.
- Client-only: hanya diketahui di browser, harus ditunda atau diberi fallback stabil.
3. Gunakan cookie atau data bootstrap bila perlu sinkron lintas server-klien
Untuk preferensi seperti tema, bahasa, atau eksperimen fitur, cookie sering lebih cocok daripada localStorage jika server perlu merender HTML awal yang akurat.
4. Prioritaskan CSS untuk responsivitas
Jika perbedaan mobile/desktop hanya presentasional, biarkan CSS yang bekerja. Ini mengurangi kebutuhan percabangan render berbasis viewport.
5. Review komponen yang menghasilkan output dinamis
Komponen seperti tanggal, countdown, personalisasi, tema, dan fitur berbasis device layak diberi perhatian khusus saat code review.
Kapan mismatch masih bisa diterima?
Secara umum, target terbaik adalah tidak ada mismatch pada UI yang penting. Namun ada kasus tertentu di mana konten memang sengaja diisi setelah mount, misalnya widget yang sepenuhnya bergantung pada browser. Dalam situasi seperti itu:
- render placeholder yang stabil di server,
- jelaskan batasannya di kode,
- pastikan perubahan setelah mount tidak merusak pengalaman pengguna.
Yang perlu dihindari adalah perubahan markup inti secara diam-diam pada fase hydration tanpa fallback yang jelas.
Penutup
Saat menghadapi UI yang berubah sendiri setelah load, jangan langsung menyalahkan SSR atau framework. Dalam banyak kasus, masalahnya adalah render awal yang tidak deterministik: membaca localStorage, memakai Date.now(), menghasilkan nilai acak, bergantung pada window, media query, atau mengganti data async terlalu cepat.
Checklist debug hydration SSR yang efektif dimulai dari satu prinsip: samakan output server dan render pertama klien, lalu pindahkan logika browser-only ke fase setelah mount atau sediakan datanya dari server. Jika tim frontend konsisten pada prinsip ini, sebagian besar hydration mismatch bisa dicegah sebelum masuk production.
Komentar
0 komentar
Masuk ke akun kamu untuk ikut berkomentar.
Belum ada komentar
Jadilah yang pertama ikut berdiskusi!