Pada halaman direktori submit website yang dinamis, SSR mismatch biasanya muncul ketika HTML hasil render di server tidak sama dengan hasil render pertama di browser. Dampaknya bukan hanya warning hydration, tetapi juga UI flicker, urutan listing yang berubah sesaat setelah halaman tampil, filter yang meloncat, atau komponen yang di-remount tanpa perlu.
Solusinya bukan mematikan SSR, melainkan memastikan state awal stabil, menunda logika yang hanya aman dijalankan di browser, dan menjaga markup server-client tetap identik pada render awal. Ini penting pada produk direktori listing seperti halaman kumpulan website submission, di mana user mengharapkan daftar, sort, filter, badge, dan metadata tampil konsisten sejak first paint.
Mengapa SSR mismatch sering terjadi pada direktori listing
Halaman direktori biasanya menggabungkan banyak sumber state: data listing dari server, preferensi user di browser, eksperimen A/B, waktu relatif, deteksi device, dan interaksi seperti sort atau filter. Jika sebagian state itu hanya tersedia di client, hasil render pertama di browser bisa berbeda dari HTML SSR.
Pada kasus direktori submit website, gejala yang sering terlihat antara lain:
- Urutan listing berubah setelah hydration karena sort diambil dari
localStorage. - Label seperti "baru 3 menit lalu" berbeda antara server dan client.
- Badge eksperimen atau CTA muncul berbeda karena flag A/B dihitung ulang di browser.
- Tampilan mobile/desktop berbeda karena server menebak user agent secara berbeda dari kondisi browser nyata.
keyitem berubah karena memakai nilai acak atau indeks array.- Markup berbeda karena komponen tertentu hanya dirender di client, tetapi SSR mengeluarkan struktur HTML lain.
Intinya: hydration mengasumsikan render awal di client identik dengan SSR. Jika asumsi ini dilanggar, framework akan memberi warning, membuang subtree tertentu, atau melakukan patch DOM yang memicu flicker.
Sumber mismatch yang paling sering dan cara menanganinya
1. Sorting dan filter berbasis localStorage
Ini sumber bug paling umum. Misalnya server merender default sort terbaru, tetapi browser menemukan preferensi lama populer di localStorage dan langsung merender urutan berbeda saat hydration.
Masalah: item A muncul di posisi 1 pada SSR, tetapi posisi 5 pada render pertama client.
Pola aman: gunakan default sort/filter yang sama di server dan client untuk render awal, lalu terapkan preferensi browser setelah mount.
// React / Next.js pattern
function DirectoryPage({ initialItems, initialSort = 'latest' }) {
const [sort, setSort] = React.useState(initialSort);
const [hydrated, setHydrated] = React.useState(false);
React.useEffect(() => {
setHydrated(true);
const saved = window.localStorage.getItem('directory.sort');
if (saved && saved !== initialSort) {
setSort(saved);
}
}, [initialSort]);
const visibleItems = React.useMemo(() => {
return sortItems(initialItems, sort);
}, [initialItems, sort]);
return (
<>
<SortControls current={sort} onChange={setSort} disabled={!hydrated} />
<DirectoryList items={visibleItems} />
</>
);
}Mengapa ini aman? Karena render awal di server dan client sama-sama memakai initialSort. Perubahan dari preferensi browser baru dilakukan setelah komponen ter-mount, sehingga tidak memicu mismatch. Konsekuensinya, urutan bisa berubah sesaat setelah mount. Untuk mengurangi flicker, tampilkan indikator bahwa preferensi personal sedang diterapkan, atau sinkronkan preferensi melalui cookie yang bisa dibaca server.
2. Data waktu dan format relatif
String seperti "5 menit lalu" sangat mudah berbeda karena server dan client menghitung pada waktu yang tidak sama. Bahkan selisih beberapa detik cukup untuk menghasilkan teks berbeda.
Pola aman:
- SSR-kan timestamp absolut yang stabil, misalnya
2026-06-26atau ISO date yang diformat konsisten. - Jika ingin format relatif, hitung di client setelah mount.
- Atau, kirim nilai waktu yang sudah dipatok dari server dan gunakan nilai itu pada render awal client.
// Hindari langsung merender relative time saat SSR jika nilainya akan dihitung ulang di client
function SubmittedAt({ isoDate }) {
const [relative, setRelative] = React.useState(null);
React.useEffect(() => {
setRelative(formatRelativeTime(isoDate));
}, [isoDate]);
return (
<time dateTime={isoDate}>
{relative ?? formatAbsoluteDate(isoDate)}
</time>
);
}Dengan pola ini, SSR dan hydration awal sama-sama menampilkan tanggal absolut yang stabil. Relative time baru menggantikan tampilan setelah client siap.
3. A/B flag atau feature flag yang tidak konsisten
Jika server memilih varian A, tetapi browser menghitung ulang dan memilih varian B pada render awal, markup akan berbeda. Ini sering terjadi jika assignment eksperimen bergantung pada penyimpanan lokal, random, atau informasi request yang tidak dibawa ke client sebagai state awal.
Pola aman: tetapkan varian di server, kirim sebagai prop/page payload, dan gunakan nilai yang sama di client sampai ada navigasi baru.
// contoh payload SSR
{
"experiment": {
"directoryCtaVariant": "A"
}
}Jangan melakukan Math.random() untuk menentukan varian di dalam komponen render. Jika assignment harus persisten, simpan hasilnya di cookie atau database lalu baca secara konsisten dari server.
4. User agent dan deteksi device
Deteksi device di server sering tidak identik dengan kondisi browser sesungguhnya. Jika SSR memilih layout mobile berdasarkan user agent, tetapi client memakai window.innerWidth dan menghasilkan layout desktop saat hydration, struktur DOM bisa berubah.
Pola aman:
- Prioritaskan desain responsif berbasis CSS dibanding conditional render yang mengubah struktur DOM.
- Jika perlu membaca ukuran viewport, lakukan setelah mount dan hindari perubahan besar pada markup awal.
- Jangan gunakan user agent untuk memutuskan struktur HTML utama kecuali benar-benar perlu.
Lebih aman menyajikan markup yang sama lalu mengatur presentasi dengan CSS daripada merender subtree yang berbeda untuk mobile dan desktop.
5. Random ID dan key yang tidak stabil
key yang berubah akan membuat item dianggap komponen baru. Pada direktori listing, ini bisa menyebabkan state item hilang, animasi aneh, atau mismatch jika urutan berubah.
Kesalahan umum:
- Memakai
Math.random()untukkey. - Memakai indeks array sebagai
keypada list yang bisa di-sort atau difilter. - Menghasilkan ID acak di server dan client secara terpisah.
Pola aman: gunakan identifier stabil dari data, misalnya slug atau ID submission.
function DirectoryList({ items }) {
return (
<ul>
{items.map((item) => (
<li key={item.id}>
<a href={item.url}>{item.name}</a>
</li>
))}
</ul>
);
}6. Perbedaan markup server-client
Meskipun datanya sama, mismatch juga bisa muncul jika struktur HTML berbeda. Contohnya:
- SSR merender
<ul><li>..., client merender<div>.... - SSR merender placeholder 10 item, client langsung merender 8 item.
- SSR punya wrapper tambahan, client tidak.
- Conditional render bergantung pada browser API saat render pertama.
Aturan praktisnya: jika bagian tertentu tidak bisa dipastikan sama di server dan client, render placeholder yang konsisten dulu.
Pola aman untuk Next.js, Nuxt, dan Inertia
Stable initial state
Semua framework SSR membutuhkan prinsip yang sama: server mengirim data dan state awal yang cukup untuk menghasilkan render pertama yang deterministik. Pada halaman direktori, state awal minimal biasanya mencakup:
- Data listing awal.
- Parameter sort/filter default.
- Pagination atau cursor aktif.
- Feature flag yang relevan.
- Nilai tanggal atau string fallback yang stabil.
Jika preferensi user penting sejak first render, pertimbangkan menyimpannya di cookie agar server bisa membacanya. Cookie bukan selalu solusi terbaik, tetapi lebih konsisten dibanding localStorage untuk kebutuhan SSR.
Defer client-only logic
Logika yang butuh window, document, localStorage, matchMedia, atau ukuran viewport sebaiknya dijalankan setelah mount.
Next.js / React: gunakan useEffect untuk membaca browser API setelah hydration.
Nuxt / Vue: jalankan di onMounted atau bungkus bagian tertentu sebagai komponen client-only jika memang tidak perlu SSR.
Inertia: karena halaman tetap di-hydrate di browser, prinsipnya sama: jangan baca browser API saat render awal jika hasilnya memengaruhi markup SSR awal.
// Vue / Nuxt pattern
const sort = ref(props.initialSort)
const hydrated = ref(false)
onMounted(() => {
hydrated.value = true
const saved = localStorage.getItem('directory.sort')
if (saved && saved !== sort.value) {
sort.value = saved
}
})Placeholder yang konsisten
Untuk mencegah flicker besar, tampilkan placeholder yang struktur HTML-nya sama di SSR dan hydration awal. Misalnya:
- Gunakan skeleton card dengan jumlah tetap.
- Untuk statistik yang bergantung pada client, tampilkan tanda strip atau label netral terlebih dahulu.
- Untuk relative time, tampilkan tanggal absolut dulu.
Yang penting bukan placeholder-nya mewah, tetapi markup awal konsisten.
Key stabil
Pada list direktori, selalu gunakan ID stabil dari backend. Jika backend belum punya identifier yang andal, perbaiki kontrak data lebih dulu. Ini bukan sekadar isu React/Vue, melainkan isu kualitas data untuk UI yang bisa diurutkan, difilter, dan dipaginasi.
Guard untuk browser API
Jangan asumsikan browser API tersedia saat SSR. Gunakan guard sederhana, tetapi pahami bahwa guard saja belum cukup jika nilainya memengaruhi render awal.
const isBrowser = typeof window !== 'undefined'
if (isBrowser) {
// aman diakses, tetapi tetap lebih baik dijalankan setelah mount
}Kesalahan umum adalah menulis guard, lalu tetap mengubah JSX/template pada render pertama client berdasarkan hasil browser API. Guard mencegah error runtime di server, tetapi belum tentu mencegah mismatch.
Contoh skenario bug nyata pada direktori submit website
Kasus 1: Sort tersimpan menyebabkan urutan loncat
Gejala: halaman kategori menampilkan urutan latest saat pertama terlihat, lalu sepersekian detik berubah ke most clicked.
Penyebab: server merender default latest, client membaca localStorage saat render awal.
Perbaikan: render awal selalu memakai initialSort, baca preferensi di effect/onMounted, dan pertimbangkan cookie jika harus konsisten sejak SSR.
Kasus 2: Badge “baru” muncul hanya di client
Gejala: warning hydration pada card tertentu, badge baru terkadang berkedip.
Penyebab: server dan client menghitung status baru berdasarkan waktu sekarang masing-masing.
Perbaikan: hitung status itu di server dan kirim sebagai field eksplisit, atau gunakan ambang waktu yang dipatok dalam payload agar client tidak menghitung ulang dengan referensi waktu berbeda.
Kasus 3: CTA eksperimen berbeda antara SSR dan browser
Gejala: teks tombol submit berubah setelah load, kadang event analytics dobel karena remount.
Penyebab: assignment A/B dilakukan ulang di client memakai random.
Perbaikan: tentukan varian di server, persist hasil assignment, dan kirim ke client sebagai state awal.
Kasus 4: Layout card berubah di mobile tertentu
Gejala: beberapa elemen berpindah tempat atau warning hydration hanya terjadi pada perangkat tertentu.
Penyebab: SSR memutuskan struktur card berdasarkan user agent, client memutuskan ulang berdasarkan viewport aktual.
Perbaikan: samakan markup dan serahkan perubahan layout ke CSS responsif sebanyak mungkin.
Checklist debugging hydration mismatch
Ketika warning terjadi, jangan langsung menebak. Periksa dengan urutan berikut:
- Bandingkan HTML SSR dan render awal client. Fokus pada area list, badge, tanggal, CTA, dan wrapper elemen.
- Audit semua sumber state. Catat mana yang berasal dari server, cookie, query string, localStorage, waktu sekarang, random, atau browser API.
- Cari operasi non-deterministik di render. Misalnya
Date.now(),new Date(),Math.random(), atau pembacaan viewport. - Periksa key list. Pastikan bukan indeks array dan tidak berubah setelah sort/filter.
- Uji dengan JavaScript lambat. Simulasikan jaringan lambat agar flicker lebih mudah terlihat.
- Matikan sementara enhancement client. Jika mismatch hilang, tambahkan kembali fitur satu per satu sampai sumbernya ketemu.
- Periksa conditional render berbasis environment. Misalnya
if (window...)atau deteksi device.
Tip praktis: warning hydration sering hanya terlihat di console development, tetapi efek visualnya tetap bisa muncul di production. Jadi jangan menganggap aman hanya karena pengguna tidak melihat error text.
Langkah verifikasi sebelum deploy
Sebelum merilis halaman direktori yang menggabungkan SSR dan enrichment di client, lakukan verifikasi berikut:
- Muat halaman dengan cache dan localStorage kosong. Pastikan SSR dan hydration awal stabil.
- Uji dengan localStorage terisi preferensi lama. Pastikan perubahan state terjadi setelah mount tanpa warning dan tanpa loncatan UI yang mengganggu.
- Uji perbedaan timezone dan locale. Format tanggal tidak boleh memicu mismatch.
- Uji perangkat mobile dan desktop. Pastikan tidak ada struktur DOM yang berubah hanya karena viewport.
- Uji eksperimen/flag aktif dan nonaktif. Markup harus konsisten dengan assignment dari server.
- Periksa view-source atau HTML response. Verifikasi bahwa server memang mengirim state awal yang diharapkan.
- Monitor warning hydration di staging. Jangan hanya mengandalkan pengujian visual.
- Uji sort, filter, dan pagination. Pastikan key item tetap stabil pada setiap transisi.
Prinsip desain yang paling aman
Jika diringkas, cara terbaik untuk mencegah SSR mismatch pada direktori submit website yang dinamis adalah memisahkan dengan tegas mana yang harus deterministik saat SSR, dan mana yang boleh diperkaya setelah client siap. Data listing utama, urutan awal, dan struktur markup harus stabil. Preferensi personal, relative time, viewport detail, dan browser-only enhancement sebaiknya ditunda sampai setelah mount.
Pendekatan ini bekerja baik di Next.js, Nuxt, maupun Inertia karena masalah dasarnya sama: hydration membutuhkan output awal yang identik. Begitu prinsip ini dijaga, warning mismatch berkurang, flicker menurun, dan halaman direktori terasa lebih solid tanpa harus mengorbankan SSR.
Komentar
0 komentar
Masuk ke akun kamu untuk ikut berkomentar.
Belum ada komentar
Jadilah yang pertama ikut berdiskusi!