Hydration queue di SSR sering menjadi akar masalah ketika UI terlihat benar saat HTML dari server pertama kali muncul, tetapi beberapa saat kemudian berubah, kehilangan event handler, atau menampilkan state yang berbeda. Masalah ini bukan sekadar “React error”, melainkan tanda bahwa server dan klien tidak memulai dari sumber kebenaran yang sama.
Untuk mencegah UI tertukar, cara paling berguna adalah memahami hydration sebagai antrean kerja bertahap: browser menerima HTML, lalu JavaScript diunduh, dijalankan, dan tiap bagian UI diikat ulang menjadi komponen interaktif. Jika selama antrean itu klien menghitung hasil yang berbeda dari server, mismatch akan muncul. Dampaknya bisa halus—teks berubah—atau lebih membingungkan—handler belum aktif, elemen lompat, atau state kembali ke nilai lain.
Memahami hydration queue sebagai antrean kerja
Hydration adalah proses ketika framework seperti React mengambil HTML hasil SSR, lalu menempelkan logika komponen dan event handler pada markup yang sudah ada. Penting dipahami: ini bukan render kosong dari nol, tetapi usaha untuk mencocokkan hasil render klien dengan HTML yang sudah dikirim server.
Analogi yang berguna adalah connection pooling. Dalam pooling, klien tidak selalu langsung berbicara ke resource akhir; ada lapisan perantara yang mengatur kapan dan bagaimana pekerjaan diproses. Pada hydration, browser juga tidak mengaktifkan seluruh halaman sekaligus. Ada urutan kerja: parse HTML, tampilkan konten awal, ambil bundle JavaScript, jalankan inisialisasi, lalu hydrate subtree atau komponen yang relevan.
Dengan sudut pandang antrean, kita bisa melihat tiga hal penting:
- Urutan matters: komponen tertentu mungkin tampak lebih dulu tetapi baru interaktif belakangan.
- Input harus identik: HTML dari server dan render awal di klien harus cocok.
- Perubahan kecil bisa berantai: satu nilai waktu atau locale yang berbeda dapat menggeser seluruh subtree.
Intinya: hydration queue bukan bug, tetapi mekanisme normal SSR. Bug muncul ketika pekerjaan dalam antrean dijalankan dengan input yang berbeda antara server dan klien.
Gejala umum saat hydration bermasalah
1. UI tampak benar lalu berubah setelah hydration
Ini gejala paling sering. Server mengirim HTML yang terlihat tepat, tetapi saat JavaScript aktif, klien melakukan render awal dengan nilai berbeda. Akibatnya teks, format angka, urutan item, atau status tombol berubah mendadak.
2. Event handler terasa terlambat atau tidak menempel
Pengguna bisa melihat tombol atau input, tetapi klik pertama tidak bereaksi, atau bereaksi setelah jeda. Ini bukan selalu masalah performa murni; bisa juga karena subtree tersebut belum selesai di-hydrate, atau React harus memperbaiki mismatch sebelum event binding stabil.
3. State awal server dan klien tidak sama
Contohnya, server merender mode default light, sementara klien membaca preferensi dark dari localStorage. UI akan muncul sebagai light, lalu berpindah ke dark setelah hydration.
4. Warning mismatch di console
Peringatan seperti teks tidak cocok, atribut berbeda, atau node berbeda adalah sinyal langsung bahwa output server dan output render pertama klien tidak identik. Jangan abaikan warning ini walaupun halaman “tetap jalan”. Dalam aplikasi kompleks, warning kecil sering berubah menjadi perilaku UI yang sulit direproduksi.
Penyebab umum hydration queue menghasilkan UI tertukar
Waktu dan zona waktu
Jika server dan browser memanggil new Date() pada waktu berbeda, hasil render bisa berbeda dalam hitungan milidetik. Masalah makin jelas saat format tanggal bergantung pada timezone server dan timezone pengguna.
// Bermasalah jika dipakai langsung di render SSR + client render
export function Clock() {
return <span>{new Date().toLocaleTimeString()}</span>;
}Kenapa ini bermasalah? Karena server merender satu nilai, lalu klien merender nilai lain saat hydration. Walaupun selisihnya kecil, string hasilnya bisa berbeda.
Locale dan formatting
toLocaleString(), format mata uang, tanggal, atau angka bisa menghasilkan output berbeda jika locale server tidak sama dengan locale browser, atau jika environment runtime memiliki konfigurasi locale yang tidak identik.
Random value
Memakai Math.random(), ID acak, atau token sementara di dalam render akan hampir selalu menghasilkan perbedaan antara server dan klien.
// Hindari pola ini di render
export function PromoBanner() {
const variant = Math.random() > 0.5 ? 'A' : 'B';
return <div>Banner {variant}</div>;
}Jika variasi UI memang harus dipilih secara acak, tentukan nilainya di server lalu kirim sebagai prop yang sama ke klien, atau hitung setelah mount jika perbedaan awal dapat ditoleransi.
Akses window atau localStorage
Server tidak memiliki window atau localStorage. Bahkan jika Anda menjaga agar kode tidak melempar error, hasil render awal tetap bisa berbeda jika klien mengambil nilai preferensi lokal saat hydration.
// Contoh yang sering memicu UI berubah setelah hydration
export function ThemeLabel() {
const theme = typeof window !== 'undefined'
? localStorage.getItem('theme') || 'light'
: 'light';
return <span>Tema: {theme}</span>;
}Server akan selalu merender light, tetapi klien bisa membaca dark. Hasilnya, UI “tertukar” setelah hydration.
Feature flag yang tidak sinkron
Jika evaluasi feature flag di server menggunakan context berbeda dari klien—misalnya cookie belum terbaca sama, user segment belum siap, atau flag diambil dari sumber async lain—maka struktur UI dapat berubah total. Ini lebih berbahaya daripada perubahan teks, karena node yang diharapkan React bisa benar-benar berbeda.
Data async yang datang pada waktu berbeda
Masalah umum di Next.js atau React SSR adalah server merender dengan data A, tetapi klien saat boot melakukan fetch ulang dan langsung mendapatkan data B. Jika render pertama klien memakai hasil berbeda itu, mismatch bisa terjadi sebelum pengguna sempat berinteraksi.
Contoh kasus React/Next.js yang memicu mismatch
Kasus 1: nilai awal dari localStorage
Pola yang aman adalah merender state awal yang stabil di server, lalu memperbaruinya setelah komponen terpasang di klien.
import { useEffect, useState } from 'react';
export function ThemeLabel() {
const [theme, setTheme] = useState('light');
const [mounted, setMounted] = useState(false);
useEffect(() => {
setMounted(true);
const saved = window.localStorage.getItem('theme');
if (saved) setTheme(saved);
}, []);
if (!mounted) {
return <span>Tema: light</span>;
}
return <span>Tema: {theme}</span>;
}Mengapa ini bekerja? Karena render server dan render awal klien sama-sama menghasilkan Tema: light. Pembacaan localStorage dipindahkan ke fase setelah mount, bukan saat hydration mencoba mencocokkan markup.
Trade-off: pengguna mungkin melihat perubahan kecil setelah mount. Jika perubahan visual ini mengganggu, pertimbangkan membaca preferensi dari cookie di server agar state awal server dan klien benar-benar sinkron.
Kasus 2: tanggal yang berubah saat render
type Props = {
isoDate: string;
};
export function PublishedAt({ isoDate }: Props) {
return <time dateTime={isoDate}>{isoDate}</time>;
}Lalu format tanggal dilakukan secara konsisten di sisi yang tepat:
- Jika perlu HTML stabil: kirim string yang sudah diformat dari server.
- Jika perlu format berdasarkan locale browser: tampilkan placeholder stabil, lalu format di klien setelah mount.
Yang penting, jangan memanggil new Date() atau formatter bergantung locale secara bebas di render awal jika output server dan klien bisa berbeda.
Kasus 3: komponen client-only di Next.js
Jika sebuah komponen memang bergantung penuh pada API browser, isolasikan sebagai komponen client-only.
import dynamic from 'next/dynamic';
const ClientChart = dynamic(() => import('./ClientChart'), {
ssr: false,
});
export default function Dashboard() {
return (
<section>
<h2>Statistik</h2>
<ClientChart />
</section>
);
}Ini cocok untuk chart interaktif, editor kaya fitur, komponen yang membaca ukuran viewport, atau widget pihak ketiga yang sulit dibuat deterministik di SSR.
Trade-off: Anda kehilangan manfaat SSR untuk komponen itu: HTML awal bisa lebih kosong, SEO berkurang untuk bagian tersebut, dan interaktivitas tetap menunggu JavaScript klien.
Alur diagnosis: cara membaca hydration queue saat terjadi mismatch
Tujuan diagnosis bukan sekadar menghilangkan warning, tetapi menemukan input pertama yang berbeda antara server dan klien. Gunakan alur berikut.
1. Temukan gejala pertama, bukan akibat terakhir
Mulai dari elemen yang berubah paling awal: teks tanggal, label tema, urutan daftar, atribut class, atau node yang hilang. Jangan langsung fokus ke komponen besar; mismatch sering berasal dari satu nilai kecil di leaf component.
2. Bandingkan sumber data render awal
Tanyakan untuk setiap nilai yang dirender:
- Apakah nilainya dihitung di server, di klien, atau keduanya?
- Apakah nilainya bergantung pada waktu sekarang?
- Apakah nilainya bergantung pada browser API?
- Apakah nilainya bisa berbeda karena locale, cookie, session, atau feature flag?
- Apakah ada fetch ulang di klien yang mengubah data terlalu cepat?
3. Audit kode render untuk operasi non-deterministik
Cari pola berikut di body komponen atau saat membuat JSX:
new Date()Date.now()Math.random()window,document,localStorage,matchMedia- formatting berbasis locale
- evaluasi feature flag yang tidak disuntikkan secara konsisten
4. Lihat peringatan di console dan minimalkan subtree
Saat ada warning hydration mismatch, sempitkan area masalah. Komentari subtree besar sementara, atau ganti komponen kompleks dengan markup statis sederhana. Jika warning hilang, Anda sudah memperkecil antrean kerja ke unit yang lebih mudah diperiksa.
5. Bedakan mismatch data dan keterlambatan event handler
Jika UI terlihat benar tetapi klik pertama tidak bekerja, penyebabnya bisa bukan data mismatch, melainkan subtree belum siap di-hydrate. Periksa ukuran bundle, banyaknya komponen client-side, dan apakah interaksi penting ditempatkan di area yang terlalu berat untuk diinisialisasi cepat.
6. Verifikasi payload awal dari server
Pada Next.js, pastikan data yang dipakai untuk render server benar-benar sama dengan data yang dibaca render awal di klien. Jika ada serialisasi props, pastikan bentuk dan nilai akhirnya tidak berubah saat diparse ulang.
Strategi isolasi client-only yang tepat
Tidak semua masalah hydration harus “disulap” menjadi SSR. Beberapa bagian memang lebih aman dijalankan hanya di klien. Kuncinya adalah memilih batas isolasi dengan sadar.
Kapan komponen sebaiknya client-only
- Sangat bergantung pada API browser.
- Output awal memang personal untuk tiap perangkat atau viewport.
- Nilainya tidak penting untuk SEO atau first paint.
- Library pihak ketiga sulit dipaksa menjadi deterministik di SSR.
Kapan sebaiknya tetap SSR
- Konten penting untuk SEO.
- Pengguna perlu melihat isi utama secepat mungkin.
- Perbedaan state awal bisa dihindari dengan menyamakan input dari server dan klien.
Pola isolasi yang umum dipakai
- Gate setelah mount: render fallback stabil sampai komponen terpasang.
- Dynamic import tanpa SSR: cocok untuk widget browser-only.
- Hydrate dari data server yang eksplisit: kirim nilai awal via props atau cookie, bukan baca ulang dari sumber lokal saat render pertama.
Praktik implementasi untuk mencegah UI tertukar
Gunakan state awal yang deterministik
Server dan klien harus punya nilai awal identik untuk render pertama. Jika nilai asli hanya tersedia di browser, render fallback stabil lalu perbarui setelah mount.
Pindahkan side effect ke useEffect
Pembacaan localStorage, pengukuran viewport, dan akses API browser lain sebaiknya tidak terjadi di render. Letakkan di useEffect agar tidak memengaruhi proses pencocokan HTML awal.
Serialisasikan data penting dari server
Jika server sudah tahu locale, timezone pilihan user, feature flag, atau preferensi tema, teruskan sebagai input eksplisit ke komponen. Ini jauh lebih aman daripada membuat klien menebak ulang.
Hindari generate ID acak di render
Jika perlu ID stabil untuk atribut aksesibilitas atau relasi elemen, gunakan mekanisme yang memang dirancang stabil lintas server dan klien, atau kirim ID dari server.
Jangan fetch ulang terlalu agresif saat boot
Jika data hasil SSR langsung ditimpa oleh fetch klien sebelum hydration stabil, UI bisa berubah sangat cepat. Gunakan data SSR sebagai sumber awal, lalu lakukan revalidasi dengan transisi yang jelas.
Checklist pencegahan hydration mismatch di SSR
- Apakah render awal memakai
new Date(),Date.now(), atau formatter berbasis locale? - Apakah ada
Math.random()atau nilai non-deterministik lain di JSX? - Apakah komponen membaca
window,document,localStorage, ataumatchMediasaat render? - Apakah feature flag dievaluasi dengan context yang sama di server dan klien?
- Apakah data async yang dipakai klien saat boot sama dengan data SSR?
- Apakah fallback awal cukup stabil sampai komponen mount?
- Apakah bagian browser-only sudah diisolasi sebagai client-only?
- Apakah warning hydration di console benar-benar nol pada skenario utama?
Kesalahan yang sering dilakukan
Menganggap warning hydration aman diabaikan
Pada aplikasi kecil mungkin tampak tidak fatal, tetapi pada halaman kompleks warning ini sering muncul bersamaan dengan event yang tertunda, layout yang meloncat, atau state yang salah setelah navigasi.
Menyembunyikan masalah dengan conditional render berlebihan
Menambahkan banyak typeof window !== 'undefined' di mana-mana sering hanya memindahkan bug, bukan memperbaiki akar masalah. Fokuslah pada kesamaan output render pertama.
Membuat seluruh halaman client-only tanpa alasan
Ini memang menghindari mismatch, tetapi juga menghilangkan manfaat SSR. Gunakan sebagai isolasi terarah, bukan solusi default.
Penutup
Menganalisis hydration queue di SSR membantu Anda melihat bahwa UI tertukar bukan kejadian acak, melainkan akibat dari pekerjaan bertahap yang dijalankan dengan input berbeda. Saat server merender HTML A tetapi klien memulai hydration dengan data B, React harus memilih: memberi warning, memperbaiki markup, atau membangun ulang bagian UI tertentu. Di situlah teks berubah, handler terasa terlambat, dan state tampak lompat.
Prinsip pencegahannya sederhana tetapi disiplin: buat render pertama deterministik, sinkronkan sumber data server-klien, pindahkan akses browser API ke fase setelah mount, dan isolasikan komponen browser-only bila perlu. Jika Anda memperlakukan hydration sebagai antrean kerja yang harus diberi input stabil, mismatch akan jauh lebih mudah didiagnosis dan dicegah.
Komentar
0 komentar
Masuk ke akun kamu untuk ikut berkomentar.
Belum ada komentar
Jadilah yang pertama ikut berdiskusi!