Hydration error karena waktu dan locale berbeda di SSR frontend biasanya terjadi saat server menghasilkan HTML awal yang tidak identik dengan render pertama di browser. Gejalanya sering terlihat sebagai warning mismatch, teks tanggal atau angka yang berubah sesaat setelah halaman tampil, atau UI yang tampak acak karena node tertentu diganti ulang oleh client.
Masalah ini umum pada aplikasi SSR modern karena server dan browser tidak selalu berbagi environment yang sama. Perbedaan timezone, locale default, format tanggal/angka, atau pemanggilan waktu saat render dapat membuat output HTML berbeda walaupun kode komponen terlihat sederhana. Kunci perbaikannya adalah memastikan render pertama bersifat deterministic: nilai yang dirender server harus sama dengan nilai yang dipakai client saat proses hydrate.
Apa yang sebenarnya terjadi saat hydration
Pada SSR, server mengirim HTML hasil render awal agar halaman cepat terlihat dan mudah diindeks. Setelah itu, JavaScript di browser melakukan hydration, yaitu menghubungkan HTML yang sudah ada dengan komponen interaktif di client.
Hydration mengasumsikan bahwa output render awal di browser akan cocok dengan HTML dari server. Jika tidak cocok, framework biasanya akan memberi warning, menambal sebagian DOM, atau dalam kasus tertentu membuang subtree dan merender ulang. Efek yang terlihat oleh pengguna bisa berupa:
- teks tanggal berubah sesaat setelah halaman muncul,
- angka mata uang berganti format,
- warning seperti text content did not match,
- komponen tertentu berkedip atau posisi UI terasa tidak stabil.
Masalahnya bukan hanya estetika. Mismatch saat hydration bisa menurunkan kepercayaan pada SSR, menyulitkan debugging, dan memicu perilaku yang sulit direproduksi karena bergantung pada environment tempat kode dijalankan.
Root cause: mengapa waktu, timezone, dan locale memicu mismatch
1. Nilai waktu tidak stabil saat render
Pemanggilan seperti new Date() atau Date.now() langsung di dalam render adalah sumber mismatch yang paling sering. Server merender pada waktu T1, lalu browser merender ulang pada waktu T2. Selisih beberapa milidetik saja cukup membuat string yang dihasilkan berbeda.
// Bermasalah: nilai berubah setiap render
function Page() {
const now = new Date();
return `<p>${now.toISOString()}</p>`;
}Jika output server berisi 2026-04-03T10:00:00.100Z dan browser menghitung 2026-04-03T10:00:00.240Z, hydration mismatch sangat mungkin terjadi.
2. Timezone server dan browser berbeda
Server SSR sering berjalan di container, VM, atau region cloud tertentu, sering kali menggunakan timezone UTC. Sementara itu browser pengguna memakai timezone lokal perangkat, misalnya Asia/Jakarta atau Europe/Berlin. Bila Anda memformat tanggal tanpa timezone eksplisit, hasilnya bisa berbeda.
// Bermasalah jika timezone tidak dikunci
function formatPublishedAt(date) {
return new Intl.DateTimeFormat(undefined, {
dateStyle: 'medium',
timeStyle: 'short'
}).format(new Date(date));
}Parameter undefined untuk locale berarti environment akan memilih default masing-masing. Begitu juga timezone akan mengikuti default environment jika tidak ditentukan. Akibatnya, server dan browser bisa menghasilkan string yang berbeda untuk timestamp yang sama.
3. Locale default tidak konsisten
Perbedaan locale memengaruhi nama bulan, urutan tanggal, pemisah desimal, simbol mata uang, dan cara pembulatan tertentu ditampilkan. Misalnya server menggunakan locale default en-US, sementara browser pengguna memakai id-ID. Nilai yang sama dapat dirender menjadi format yang berbeda total.
// Bermasalah jika locale mengandalkan default environment
function Price({ amount }) {
return new Intl.NumberFormat().format(amount);
}Server bisa menghasilkan 1,234.56, sedangkan browser menghasilkan 1.234,56.
4. Relative time sangat mudah berubah
Format seperti 5 detik lalu, 2 menit lalu, atau baru saja sangat rentan mismatch karena nilainya memang bergerak seiring waktu. SSR yang merender relative time hampir selalu berisiko jika tidak memakai strategi khusus.
// Bermasalah untuk SSR jika langsung dihitung saat render
function RelativeTime({ createdAt }) {
const diffInSeconds = Math.floor((Date.now() - new Date(createdAt).getTime()) / 1000);
return `<span>${diffInSeconds} detik lalu</span>`;
}5. Parsing tanggal yang ambigu
String tanggal yang tidak eksplisit, misalnya tanpa offset timezone atau format yang ambigu, dapat diparse berbeda antar environment. Bahkan jika tidak memicu mismatch langsung, ini sering menjadi sumber bug yang terlihat seperti masalah hydration.
// Hindari format ambigu
new Date('2026-04-03 10:00:00');Lebih aman gunakan timestamp Unix, ISO 8601 dengan timezone jelas, atau nilai yang sudah dinormalisasi di backend.
Pola kode yang sering menyebabkan hydration error
Render langsung dengan nilai environment-dependent
function Header() {
return `<div>${new Date().toLocaleString()}</div>`;
}Masalahnya ada dua: waktu berubah setiap render, dan toLocaleString() bergantung pada locale serta timezone default environment.
Menggunakan locale implisit
function Stats({ value }) {
return `<strong>${value.toLocaleString()}</strong>`;
}Walaupun datanya sama, hasil format bisa berbeda antara server dan client.
Menampilkan relative time pada render pertama
function CommentMeta({ createdAt }) {
const minutes = Math.floor((Date.now() - new Date(createdAt).getTime()) / 60000);
return `<span>${minutes} menit lalu</span>`;
}Render pertama di server hampir pasti tidak akan identik dengan render pertama di browser, apalagi bila hydration tertunda karena jaringan atau beban CPU.
Mengandalkan timezone lokal tanpa data dari user
Jika server tidak tahu timezone pengguna, tetapi komponen memformat waktu seolah-olah tahu, maka output awal SSR rawan salah atau tidak konsisten. Ini sering terlihat pada halaman dashboard, feed, riwayat transaksi, dan jadwal.
Strategi perbaikan yang aman dan praktis
1. Render nilai stabil di server
Prinsip utama: server dan client harus menerima nilai awal yang sama. Jika tanggal sudah tersedia dari backend, kirim nilai mentah yang stabil seperti ISO string UTC atau timestamp Unix, lalu gunakan itu secara konsisten.
// Backend/API mengirim nilai stabil
{
"publishedAt": "2026-04-03T10:00:00.000Z"
}Lalu untuk render awal SSR, pilih format yang juga stabil. Misalnya tampilkan ISO pendek, atau format eksplisit dengan locale dan timezone yang sama di kedua sisi.
function formatUtcDate(isoString) {
return new Intl.DateTimeFormat('id-ID', {
timeZone: 'UTC',
year: 'numeric',
month: '2-digit',
day: '2-digit',
hour: '2-digit',
minute: '2-digit'
}).format(new Date(isoString));
}Pendekatan ini cocok jika Anda memang ingin output awal yang identik, misalnya untuk halaman artikel, log, invoice, atau data historis yang tidak harus mengikuti timezone lokal user pada saat pertama tampil.
2. Tunda format client-only sampai setelah hydration
Jika Anda memang perlu format lokal pengguna, jangan paksa server menebak. Render placeholder atau nilai netral di server, lalu setelah komponen sudah berjalan di browser, format ulang menggunakan locale dan timezone dari client.
// Contoh pola umum, tidak bergantung pada framework tertentu
function renderTimestamp(isoString, isClient) {
if (!isClient) {
return '<time datetime="' + isoString + '">--</time>';
}
const formatted = new Intl.DateTimeFormat('id-ID', {
dateStyle: 'medium',
timeStyle: 'short'
}).format(new Date(isoString));
return '<time datetime="' + isoString + '">' + formatted + '</time>';
}Keuntungannya, Anda menghindari mismatch karena server tidak mencoba menghasilkan string yang bergantung pada environment user. Trade-off-nya adalah pengguna melihat placeholder sesaat sampai client selesai hydrate.
Untuk data yang sangat sensitif terhadap waktu seperti relative time, placeholder atau format absolut pada SSR hampir selalu lebih aman daripada langsung merender versi relatif.
3. Kirim timezone dan locale secara eksplisit
Jika aplikasi Anda membutuhkan SSR yang benar-benar sesuai preferensi pengguna, kirim locale dan timezone sebagai bagian dari request context. Sumbernya bisa dari preferensi akun, cookie, header, atau deteksi client yang kemudian disimpan untuk request berikutnya.
// Contoh data konteks request yang dipakai server dan client
{
"locale": "id-ID",
"timeZone": "Asia/Jakarta"
}Lalu gunakan nilai ini secara eksplisit di semua formatter.
function formatCurrency(amount, locale, currency) {
return new Intl.NumberFormat(locale, {
style: 'currency',
currency
}).format(amount);
}
function formatDate(isoString, locale, timeZone) {
return new Intl.DateTimeFormat(locale, {
timeZone,
dateStyle: 'medium',
timeStyle: 'short'
}).format(new Date(isoString));
}Pendekatan ini paling baik untuk aplikasi yang punya konsep user preference yang jelas, seperti produk SaaS, dashboard internal, atau aplikasi multi-region. Trade-off-nya adalah Anda perlu memastikan konteks request tersebut tersedia konsisten di SSR dan client.
4. Gunakan placeholder yang aman dan tidak menipu
Placeholder bukan sekadar teks kosong. Pilih placeholder yang tidak memberikan informasi salah dan tidak menyebabkan layout bergeser terlalu jauh. Contoh yang aman:
--untuk jam yang belum diformat,- skeleton dengan lebar mendekati hasil akhir,
- tanggal absolut stabil di SSR, lalu diganti menjadi format lokal di client.
Misalnya, server menampilkan 2026-04-03 10:00 UTC, lalu browser mengganti menjadi 3 Apr 2026, 17.00 WIB setelah hydration. Ini lebih baik daripada server menampilkan format lokal yang salah.
5. Pisahkan data mentah dari layer formatting
Sering kali masalah bukan ada pada datanya, tetapi karena formatting tercampur dengan render komponen. Simpan data mentah dalam bentuk yang netral, lalu buat utilitas formatter yang menerima locale dan timeZone eksplisit.
// Data mentah tetap netral
const order = {
createdAt: '2026-04-03T10:00:00.000Z',
total: 1250000.5
};
// Formatting dipusatkan
function formatOrder(order, locale, timeZone) {
return {
createdAtLabel: new Intl.DateTimeFormat(locale, {
timeZone,
dateStyle: 'medium',
timeStyle: 'short'
}).format(new Date(order.createdAt)),
totalLabel: new Intl.NumberFormat(locale, {
style: 'currency',
currency: 'IDR'
}).format(order.total)
};
}Dengan pola ini, Anda lebih mudah mengaudit bagian mana yang berpotensi tidak deterministik.
Kapan memilih tiap pendekatan
Pilih render stabil di server jika:
- Anda butuh HTML awal yang identik dan minim risiko mismatch.
- Data bersifat historis atau tidak perlu mengikuti locale pengguna saat pertama tampil.
- Anda ingin SSR yang sederhana dan mudah diuji.
Pilih format client-only jika:
- Output harus mengikuti timezone atau locale aktual perangkat pengguna.
- Data berupa relative time atau nilai yang berubah cepat.
- Anda menerima adanya placeholder singkat setelah load awal.
Pilih kirim locale/timezone eksplisit jika:
- Aplikasi memiliki preferensi pengguna yang tersimpan.
- Anda ingin SSR yang personal tetapi tetap konsisten.
- Anda siap mengelola context request dan sinkronisasi state awal.
Kesalahan umum yang sering terlewat
- Menganggap
toLocaleString()aman untuk SSR. Padahal default locale dan timezone bisa berbeda antar environment. - Memanggil
Date.now()di body render. Ini membuat render tidak deterministik. - Menggunakan relative time pada SSR tanpa fallback. Nilainya bergerak setiap detik.
- Mengirim string tanggal ambigu dari backend. Pastikan format tanggal menyertakan timezone yang jelas.
- Memformat angka atau mata uang tanpa locale eksplisit. Angka desimal dan separator mudah berubah.
- Mengira warning hydration hanya masalah development. Walau warning lebih terlihat saat development, akar masalahnya tetap bisa memengaruhi perilaku UI produksi.
Tips debugging: bedakan masalah data, environment, dan formatting
Periksa output mentah dari server
Lihat HTML yang benar-benar dikirim server, bukan hanya hasil DOM setelah JavaScript berjalan. Pastikan teks yang dirender server memang sesuai dengan yang Anda duga.
Bandingkan input formatter di server dan client
Log nilai mentah yang dipakai untuk render pertama: timestamp, locale, timezone, currency, dan opsi formatter. Jika inputnya berbeda, masalah ada pada aliran data atau context request.
Log environment yang relevan
Periksa timezone dan locale yang dipakai masing-masing sisi. Dalam praktiknya, Anda ingin tahu:
- apakah server berjalan di UTC atau timezone lain,
- locale default apa yang aktif di browser pengguna,
- apakah request membawa preference user atau tidak.
Matikan sementara formatting kompleks
Ganti hasil formatter dengan nilai mentah seperti ISO string atau angka polos. Jika mismatch hilang, sumber masalah hampir pasti ada pada layer formatting, bukan pada data fetch.
Curigai relative time lebih dulu
Jika warning terlihat acak dan sulit direproduksi, cek semua tempat yang menghitung umur waktu seperti x detik lalu, countdown, atau jam real-time. Jenis tampilan ini sering menjadi penyebab utama.
Pola implementasi yang lebih aman secara umum
- Backend mengirim waktu dalam format netral dan eksplisit, misalnya ISO UTC.
- SSR hanya merender nilai yang stabil, atau placeholder jika perlu format lokal pengguna.
- Client melakukan format lokal setelah hydration, atau memakai locale/timezone yang sudah dikirim eksplisit dari server.
- Semua formatter menerima parameter
localedantimeZone, bukan mengandalkan default environment. - Relative time diupdate di client, bukan dijadikan output SSR utama.
Pola ini bekerja karena ia menghilangkan sumber ketidakpastian saat render awal. Selama server dan browser memulai dari data mentah yang sama dan aturan formatting yang sama, hydration akan jauh lebih stabil.
Checklist debugging singkat
- Data: apakah timestamp/angka yang diterima server dan client benar-benar sama?
- Environment: apakah timezone dan locale default server berbeda dengan browser?
- Formatting: apakah ada
toLocaleString(),Intl, atauDate.now()di render awal? - Relative time: apakah ada tampilan seperti baru saja atau x menit lalu yang dirender saat SSR?
- Placeholder: jika data perlu format lokal user, apakah SSR memakai fallback yang aman?
- Parsing: apakah string tanggal yang dikirim backend eksplisit dan tidak ambigu?
- Context request: jika locale/timezone user tersedia, apakah nilainya dipakai konsisten di server dan client?
Jika Anda menemukan hydration error yang tampak acak, mulai dari asumsi paling sederhana: render pertama Anda tidak deterministik. Pada kasus waktu, timezone, locale, dan format tanggal/angka, solusi terbaik hampir selalu bukan “menyembunyikan warning”, melainkan memastikan server dan client merender nilai awal yang sama atau secara sengaja menunda formatting ke client dengan placeholder yang aman.
Komentar
0 komentar
Masuk ke akun kamu untuk ikut berkomentar.
Belum ada komentar
Jadilah yang pertama ikut berdiskusi!