State hydration stabil di SSR berarti HTML yang dirender server harus cocok dengan state awal yang dipakai client saat proses hydrate. Jika store client-side menghasilkan nilai berbeda pada render pertama, Anda akan melihat gejala seperti teks berubah setelah halaman interaktif, flash status login, theme berganti mendadak, counter tidak sama, atau daftar hasil fetch berubah sesaat setelah load.
Masalah ini bukan sekadar warning kosmetik. Hydration mismatch membuat UI terasa tidak konsisten, menyulitkan debugging, dan pada kasus tertentu menyebabkan event binding atau struktur DOM tidak berjalan seperti yang diharapkan. Intinya sederhana: state awal harus deterministik dan identik antara server dan client. Prinsip ini mirip dengan perhatian pada determinisme data dan memori yang juga sering dibahas di ekosistem bahasa modern seperti Odin: perilaku awal yang eksplisit jauh lebih mudah diprediksi daripada state yang muncul diam-diam dari lingkungan runtime.
Apa yang Sebenarnya Terjadi Saat Hydration
Pada SSR, server mengirim HTML hasil render awal. Setelah JavaScript di browser berjalan, framework melakukan hydration: menghubungkan komponen, event handler, dan store ke HTML yang sudah ada. Agar proses ini aman, output render pertama di client harus sama dengan output server.
Mismatch terjadi ketika:
- server merender nilai A,
- tetapi client pada render pertama langsung memakai nilai B.
Contoh yang sering terjadi:
- Flash state login: server menganggap user belum login, tetapi client langsung membaca token dari
localStoragedan mengubah UI menjadi “Halo, Budi”. - Theme berbeda: server merender mode terang, client langsung memaksa mode gelap dari storage.
- Counter berubah: store membuat nilai awal dengan
Date.now()atau angka acak. - List hasil fetch tidak sinkron: server dan client melakukan fetch yang sama tetapi hasil, urutan, atau waktu penyelesaiannya berbeda.
Root Cause Paling Umum
1. Nilai non-deterministik di state awal
Semua nilai berikut berpotensi menimbulkan mismatch bila dipakai langsung untuk render awal:
Math.random()Date.now()- waktu lokal berdasarkan timezone browser
- format tanggal/angka berbasis locale default environment
Jika server dan client menghitung nilai awal secara independen, hasilnya belum tentu sama. Ini sangat sering terjadi pada store yang diinisialisasi otomatis saat modul di-load.
2. Akses window atau localStorage terlalu awal
Server tidak punya window, document, atau localStorage. Solusi yang keliru biasanya berupa fallback default saat SSR, lalu nilai berubah saat client membaca browser API. Secara teknis kode bisa “jalan”, tetapi HTML server dan state client pertama tetap berbeda.
3. Perbedaan timezone dan locale
Render tanggal seperti “10/11/2026” atau “11 Okt 2026, 08.00” bisa berbeda jika server dan browser memakai locale atau timezone yang tidak sama. Jika string itu tampil langsung pada SSR, mismatch dapat terjadi meskipun data mentahnya identik.
4. Race condition saat fetch
Server melakukan fetch untuk SSR, lalu client melakukan fetch lagi saat mount. Jika hasilnya berubah, urutannya berbeda, atau request kedua selesai lebih cepat dengan state lain, UI akan berkedip atau mengganti isi list sesaat setelah hydrate.
5. Inisialisasi store ganda
Pola store singleton yang aman di SPA bisa bermasalah di SSR. Jika store dibuat di scope modul global tanpa mekanisme state per-request, Anda berisiko mendapatkan:
- state bocor antar-request di server,
- state diinisialisasi dua kali,
- state server tidak sama dengan payload yang dipakai client.
Gejala Nyata yang Perlu Diwaspadai
- Teks berubah setelah hydrate, misalnya “Selamat pagi” menjadi “Selamat malam”.
- Flash login state, awalnya tombol “Masuk”, lalu berubah menjadi avatar user.
- Counter atau badge berbeda antara HTML awal dan UI setelah interaktif.
- Theme flicker, halaman muncul terang lalu tiba-tiba gelap.
- List hasil fetch meloncat, jumlah item berubah, urutan pindah, atau placeholder muncul lagi.
- Warning hydration di console framework, biasanya menyebut text/content mismatch.
Langkah Diagnosis yang Efektif
Bandingkan sumber state pertama
Tanyakan tiga hal berikut untuk setiap data yang tampil pada SSR:
- Dari mana nilai saat server render berasal?
- Dari mana nilai saat client render pertama berasal?
- Apakah keduanya dijamin sama?
Jika jawabannya “tidak yakin”, di situlah sumber mismatch paling mungkin.
Log state sebelum dan sesudah hydrate
Tambahkan logging terarah, bukan logging acak. Fokus pada state yang memengaruhi HTML awal:
console.log('server initial state', initialStateFromServer)
console.log('client first state', store.getState())Untuk framework SSR, bedakan log yang berjalan di server dan di browser. Tujuannya adalah memastikan payload awal benar-benar sama.
Audit semua akses browser-only API
Cari penggunaan:
windowdocumentlocalStoragesessionStoragematchMedia
Jika dipakai untuk menentukan render awal, besar kemungkinan itu penyebab masalah.
Periksa apakah fetch dijalankan dua kali
Banyak mismatch bukan karena data salah, tetapi karena request SSR dan request client tidak memakai payload yang sama. Pastikan hasil fetch server diserialisasi ke client, lalu dipakai sebagai state awal alih-alih mengambil ulang tanpa alasan.
Waspadai singleton di server
Jika store dibuat sekali di modul dan dipakai ulang untuk semua request, itu tanda bahaya pada SSR. State harus dibuat per-request kecuali Anda benar-benar menyimpan data immutable yang tidak bergantung pada user.
Pola Perbaikan yang Paling Aman
1. Gunakan server sebagai sumber kebenaran untuk render awal
Prinsip utama: semua data yang memengaruhi HTML awal sebaiknya ditentukan di server, lalu dikirim ke client sebagai initial state. Client boleh memperbarui state setelah hydrate, tetapi render pertama harus memakai payload yang sama.
Contoh pola umum:
// pseudo-code
const initialState = await buildInitialStateFromRequest(req)
const html = renderApp({ initialState })
// serialisasikan initialState ke HTML
// di client
const initialState = readSerializedState()
const store = createStore(initialState)
hydrateApp({ store })Mengapa ini bekerja? Karena server dan client tidak lagi menghitung state awal secara terpisah. Keduanya memakai snapshot yang sama.
2. Tunda pembacaan browser storage sampai setelah mount jika memang browser-only
Jika suatu nilai memang hanya ada di browser, jangan biarkan nilai itu memengaruhi HTML SSR secara langsung. Render dulu fallback yang stabil, lalu sinkronkan setelah mount.
Contoh kasus theme:
- jika theme harus akurat sejak respons pertama, kirim preferensi melalui cookie agar server bisa merender theme yang benar;
- jika cukup disesuaikan setelah interaktif, gunakan fallback stabil dan update setelah mount.
Trade-off: menunda pembacaan storage mengurangi mismatch, tetapi dapat menimbulkan flicker. Jika Anda ingin menghilangkan flicker, pindahkan sumber data ke request yang bisa dibaca server, misalnya cookie.
3. Normalisasi timezone dan locale
Jangan format string tanggal yang sensitif terhadap environment secara sembarang di kedua sisi. Pilihan aman:
- format di server dan kirim string final yang sama ke client;
- atau render placeholder netral lalu format setelah mount;
- atau pastikan locale dan timezone yang dipakai eksplisit, bukan default environment.
Data mentah seperti ISO timestamp lebih aman daripada string yang diformat secara implisit.
4. Hindari inisialisasi store dengan efek samping
createStore() sebaiknya menerima state awal sebagai input, bukan membaca lingkungan global secara diam-diam. Store yang mudah diuji biasanya juga lebih aman untuk SSR.
Lebih baik:
function createStore(initialState) {
return {
state: initialState,
// actions...
}
}Kurang aman untuk SSR:
function createStore() {
return {
theme: localStorage.getItem('theme') || 'light',
count: Date.now()
}
}5. Reuse payload fetch SSR, jangan refetch buta saat hydrate
Jika data sudah diambil saat SSR, simpan hasilnya sebagai initial state. Fetch ulang hanya jika memang perlu revalidasi. Bahkan saat revalidasi diperlukan, tampilkan dulu data SSR agar tidak terjadi loncatan UI yang tidak perlu.
6. Buat store per-request pada SSR
Untuk framework yang mengizinkan proses server tetap hidup lama, jangan anggap state global aman. State yang terkait request, user, locale, atau cookie harus dibuat ulang untuk setiap request.
Contoh Praktis
Next.js: jangan baca localStorage di state awal store
Contoh masalah umum:
// jangan lakukan ini untuk state awal SSR
const initialTheme = typeof window !== 'undefined'
? localStorage.getItem('theme') || 'light'
: 'light'Kode di atas terlihat aman karena ada pengecekan window, tetapi tetap bisa mismatch. Server selalu merender light, sementara client bisa langsung memakai dark.
Pola yang lebih stabil:
- simpan theme di cookie agar server dapat membaca preferensi user;
- pass nilai itu sebagai initial state ke komponen/store;
- gunakan
localStoragehanya sebagai sinkronisasi tambahan setelah mount bila perlu.
// pseudo-code
export async function getServerSideProps(ctx) {
const theme = readThemeFromCookie(ctx.req) || 'light'
return { props: { initialTheme: theme } }
}
function Page({ initialTheme }) {
const store = useMemo(() => createStore({ theme: initialTheme }), [initialTheme])
return <App store={store} />
}Untuk status login, prinsipnya sama: jangan tentukan UI awal dari token di browser jika server belum mengetahuinya. Gunakan cookie sesi atau mekanisme auth yang dapat divalidasi di server.
Nuxt: isi state dari server payload, bukan dari akses browser lebih awal
Pada Nuxt, pola aman adalah mengambil data dan membangun state lewat jalur SSR framework, lalu membiarkan client memakai payload yang sama saat hydrate. Hindari menentukan state awal di plugin universal yang langsung membaca localStorage atau window.
// pseudo-code
export default defineNuxtPlugin(async (nuxtApp) => {
// untuk data SSR, ambil dari request/server context atau payload framework
// jangan menetapkan state awal dari localStorage di sini jika memengaruhi HTML awal
})Jika Anda butuh preferensi browser-only, jalankan logika itu di hook client-only setelah mount, dan pastikan fallback SSR sudah stabil.
SvelteKit: pisahkan data load dari pembacaan browser-only
Di SvelteKit, data yang dibutuhkan untuk render awal sebaiknya datang dari load atau sumber server-side lain yang dapat diserialisasi ke halaman. Jangan isi store awal dari browser API jika nilai itu akan memengaruhi markup SSR.
// pseudo-code
export async function load(event) {
const session = await readSessionFromCookie(event.cookies)
const items = await fetchItemsForSSR(event)
return { session, items }
}Setelah itu, komponen dan store memakai data dari load sebagai initial state. Jika perlu membaca theme dari browser, lakukan setelah komponen mounted dan hindari perubahan struktur besar pada markup awal.
Kasus-Kasus Umum dan Solusi Singkat
Flash status login
Penyebab: server tidak tahu user login, client tahu dari storage.
Solusi: pindahkan sumber kebenaran login ke cookie/sesi yang bisa dibaca server. Render awal harus berdasar status auth yang sama di kedua sisi.
Theme flicker
Penyebab: server merender default theme, client mengganti dari storage saat hydrate.
Solusi: gunakan cookie untuk preferensi theme, atau render fallback netral lalu update setelah mount. Jika tema memengaruhi class root, lakukan inisialisasi sedini mungkin dengan sumber yang sama.
Counter berbeda
Penyebab: nilai awal memakai waktu sekarang atau angka acak.
Solusi: hitung di server sekali lalu kirim nilainya ke client, atau jangan tampilkan nilai tersebut pada SSR.
List fetch tidak sinkron
Penyebab: fetch SSR dan fetch client berjalan independen.
Solusi: serialisasikan hasil fetch server ke initial state. Revalidasi setelah hydrate hanya bila dibutuhkan.
Teks tanggal berubah
Penyebab: perbedaan locale/timezone server dan browser.
Solusi: gunakan format eksplisit, kirim string final dari server, atau tunda formatting ke client dengan placeholder yang aman.
Checklist Implementasi
- Identifikasi semua state yang memengaruhi HTML awal.
- Pastikan sumber data render pertama sama di server dan client.
- Serialisasikan initial state SSR dan gunakan state itu untuk membuat store di client.
- Jangan baca browser API saat menentukan initial render, kecuali komponen client-only.
- Gunakan cookie atau request context untuk auth, locale, dan preferensi yang harus konsisten sejak awal.
- Normalisasi tanggal, locale, dan timezone.
- Buat store per-request pada SSR, hindari singleton mutable di server.
- Hindari refetch tanpa kontrol tepat saat hydrate jika data SSR sudah tersedia.
Debugging Tips yang Sering Membantu
- Lihat HTML awal yang dikirim server, bukan hanya DOM setelah interaktif.
- Nonaktifkan JavaScript sementara untuk memverifikasi output SSR murni.
- Bandingkan payload serialized state dengan state store setelah bootstrap client.
- Cari state yang berubah pada tick pertama setelah mount; itulah kandidat mismatch.
- Waspadai utilitas global yang diam-diam membaca waktu, locale, atau storage.
Catatan: Tidak semua perubahan setelah mount adalah bug. Masalah muncul ketika perubahan itu mengubah hasil render pertama yang seharusnya cocok dengan HTML SSR. Perbaruan data setelah hydrate boleh saja, asalkan terjadi sebagai langkah berikutnya yang jelas, bukan karena state awal yang tidak sinkron.
Kapan Boleh Memilih Client-Only?
Jika suatu bagian UI sepenuhnya bergantung pada browser API dan tidak penting untuk SSR, pendekatan client-only bisa lebih sederhana. Contohnya widget yang hanya berguna setelah interaktif atau fitur personalisasi kecil. Namun ini adalah keputusan arsitektur, bukan tambalan universal.
Trade-off client-only:
- lebih mudah menghindari mismatch;
- tetapi mengurangi manfaat SSR untuk bagian tersebut;
- bisa menambah loading placeholder atau layout shift jika tidak dirancang baik.
Penutup
Cara paling konsisten untuk menghindari hydration mismatch dari store client-side adalah memastikan state awal bersifat deterministik, dibuat dari sumber yang sama, dan dipakai ulang di server maupun client. Jangan biarkan store menentukan nilai awal dari browser API, waktu, locale, atau fetch independen jika state itu memengaruhi HTML SSR.
Untuk Next.js, Nuxt, dan SvelteKit, pola dasarnya sama: bangun initial state dari request di server, serialisasikan hasilnya, buat store client dari payload itu, lalu lakukan sinkronisasi lanjutan secara eksplisit setelah hydrate. Jika Anda disiplin pada prinsip ini, gejala seperti teks berubah, flash login, theme flicker, dan list fetch yang meloncat biasanya hilang dengan sendirinya.
Komentar
0 komentar
Masuk ke akun kamu untuk ikut berkomentar.
Belum ada komentar
Jadilah yang pertama ikut berdiskusi!