Render mismatch SSR akibat locale dan waktu di browser terjadi ketika HTML yang dirender server tidak identik dengan hasil render awal React di browser. Kasus yang umum adalah server menampilkan 08/03/2025, 14.30, sedangkan browser menampilkan 3/8/2025, 9:30 AM. Saat hydration, React membandingkan keduanya dan dapat mengeluarkan warning atau mengganti bagian DOM tertentu.

Perbaikannya bukan sekadar menyembunyikan warning. Tentukan dengan jelas siapa yang bertanggung jawab memformat nilai, gunakan locale dan timezone eksplisit bila tampilan harus konsisten, atau tunda rendering nilai yang benar-benar bergantung pada environment browser hingga setelah komponen di-mount.

Memahami sumber hydration mismatch

SSR menghasilkan HTML di environment server, umumnya Node.js atau runtime serverless. Setelah HTML tiba di browser, React menjalankan render awal lagi untuk melakukan hydration. Untuk komponen yang di-hydrate, keluaran render awal di kedua sisi harus sama.

Masalah muncul ketika kode render bergantung pada nilai yang berbeda antar-environment, misalnya:

  • Timezone default berbeda. Server dapat berjalan dalam UTC, sedangkan pengguna berada di Asia/Jakarta atau America/New_York.
  • Locale default berbeda. Server mungkin memilih locale sistem atau locale deployment, sementara browser memakai preferensi pengguna seperti id-ID atau en-US.
  • Intl memakai default runtime. new Intl.DateTimeFormat(undefined) meminta runtime memilih locale default. Pilihan itu tidak dijamin sama antara server dan browser.
  • Waktu berubah selama render. new Date(), Date.now(), atau teks relatif seperti baru saja dapat berubah di antara SSR dan hydration.
  • Perbedaan environment Node dan browser. Data locale, konfigurasi timezone host, versi runtime, atau implementasi internasionalisasi yang tersedia dapat membuat output format berbeda.

Contoh berikut tampak aman, tetapi berisiko mismatch karena locale dan timezone dibiarkan dipilih otomatis oleh masing-masing runtime.

'use client';

export function LocalTime({ iso }) {
  const label = new Intl.DateTimeFormat(undefined, {
    dateStyle: 'medium',
    timeStyle: 'short'
  }).format(new Date(iso));

  return <time dateTime={iso}>{label}</time>;
}

Misalnya, server merender <time>Mar 8, 2025, 2:30 PM</time>, tetapi browser pengguna merender <time>8 Mar 2025, 21.30</time>. Karena komponen client juga dapat dirender menjadi HTML pada server sebelum di-hydrate, perbedaan ini terjadi pada render awal, bukan hanya setelah interaksi pengguna.

Mendiagnosis render mismatch SSR akibat locale dan waktu

1. Baca warning hydration, bukan hanya gejalanya

Periksa console browser pada mode development. Warning React biasanya menunjukkan node teks atau subtree yang tidak cocok. Fokuskan investigasi pada komponen yang menampilkan tanggal, angka, mata uang, teks relatif, atau nilai yang dibuat dari waktu saat ini.

Jangan langsung menganggap masalah berasal dari CSS atau navigasi Next.js. Jika warning menyebut konten teks berbeda, bandingkan nilai yang diproduksi oleh server dan browser untuk properti yang sama.

2. Bandingkan HTML respons awal dengan DOM setelah hydration

Buka tab Network di browser, pilih dokumen HTML, lalu periksa isi respons awal. Anda juga dapat memakai View Source atau mengambil respons dari server dengan curl. Pada aplikasi yang memakai streaming, struktur respons dapat berisi data tambahan framework, tetapi teks hasil SSR tetap dapat dicari.

curl -s http://localhost:3000/events/42 | grep -n '2025'

Kemudian bandingkan dengan elemen yang sama di tab Elements setelah JavaScript berjalan. Jika teks berubah dari format server ke format browser tanpa data API baru, formatter di render awal adalah tersangka utama.

3. Catat input formatter di kedua environment

Periksa tiga input ini: nilai mentah, locale, dan timezone. Nilai mentah harus berupa instant yang jelas, misalnya ISO 8601 dengan offset atau akhiran Z. Locale dan timezone juga harus diketahui, bukan hanya diasumsikan dari mesin yang menjalankan kode.

const date = new Date(iso);

console.log({
  iso,
  epochMs: date.getTime(),
  resolved: new Intl.DateTimeFormat().resolvedOptions()
});

Log server muncul di log proses server, sedangkan log browser muncul di DevTools. Hindari menambahkan percabangan typeof window yang menghasilkan markup berbeda langsung di dalam render; percabangan semacam itu sendiri dapat menciptakan mismatch.

Pola perbaikan yang aman

Kirim teks yang sudah diformat dari server

Pilih pola ini bila semua pengguna harus melihat format yang sama, misalnya dashboard operasional dengan timezone organisasi. Server menentukan kebijakan presentasi, lalu komponen client menerima teks yang sudah final. Dengan demikian, hydration tidak perlu menjalankan formatter lagi.

// app/events/[id]/page.jsx - Server Component
import { EventTime } from './EventTime';

function formatEventTime(iso) {
  return new Intl.DateTimeFormat('id-ID', {
    dateStyle: 'long',
    timeStyle: 'short',
    timeZone: 'Asia/Jakarta'
  }).format(new Date(iso));
}

export default async function EventPage() {
  const event = {
    startsAt: '2025-03-08T14:30:00.000Z'
  };

  return (
    <EventTime
      iso={event.startsAt}
      label={formatEventTime(event.startsAt)}
    />
  );
}
// app/events/[id]/EventTime.jsx
'use client';

export function EventTime({ iso, label }) {
  return <time dateTime={iso}>{label}</time>;
}

Pola ini juga cocok jika komponen membutuhkan interaksi client lain, tetapi teks awal tidak boleh berubah. Simpan nilai ISO pada atribut dateTime agar markup tetap semantik dan tersedia untuk kebutuhan JavaScript berikutnya.

Gunakan locale dan timezone eksplisit

Jika format memang harus deterministik pada kedua sisi, pakai parameter yang identik untuk Intl.DateTimeFormat atau Intl.NumberFormat. Jangan gunakan undefined sebagai locale dan jangan bergantung pada timezone default proses.

export function formatPrice(amount) {
  return new Intl.NumberFormat('id-ID', {
    style: 'currency',
    currency: 'IDR',
    maximumFractionDigits: 0
  }).format(amount);
}

export function formatUtcTime(iso) {
  return new Intl.DateTimeFormat('id-ID', {
    dateStyle: 'medium',
    timeStyle: 'short',
    timeZone: 'UTC'
  }).format(new Date(iso));
}

Strategi ini tepat untuk invoice, laporan, jadwal dalam timezone bisnis, atau konten publik yang harus konsisten. Konsekuensinya, format tidak otomatis mengikuti locale dan timezone pengguna.

Catatan: bedakan instant dengan tanggal lokal. Nilai seperti 2025-03-08T14:30:00Z adalah satu titik waktu global. Sebaliknya, nilai seperti 2025-03-08 atau jam janji temu tanpa offset memerlukan aturan bisnis yang eksplisit. Jangan mengandalkan parsing implisit untuk menentukan timezone sebuah tanggal lokal.

Tunda format khusus browser sampai setelah mount

Gunakan pola ini bila produk memang harus menampilkan timezone atau locale perangkat pengguna. Render placeholder yang sama di server dan pada render client pertama, lalu format nilai lokal di dalam effect. Dengan begitu, perubahan terjadi setelah hydration selesai.

'use client';

import { useEffect, useState } from 'react';

export function BrowserLocalTime({ iso }) {
  const [mounted, setMounted] = useState(false);

  useEffect(() => {
    setMounted(true);
  }, []);

  const label = mounted
    ? new Intl.DateTimeFormat(navigator.language, {
        dateStyle: 'medium',
        timeStyle: 'short'
      }).format(new Date(iso))
    : 'Memuat waktu lokal…';

  return <time dateTime={iso}>{label}</time>;
}

Trade-off-nya adalah pengguna melihat placeholder singkat dan berpotensi mengalami layout shift. Sediakan ruang yang cukup, gunakan teks fallback yang masuk akal, atau tampilkan timezone organisasi terlebih dahulu bila stabilitas tampilan lebih penting daripada personalisasi instan.

Gunakan suppressHydrationWarning secara sangat terbatas

suppressHydrationWarning dapat dipakai untuk node teks yang memang tidak mungkin identik, misalnya jam berjalan yang sengaja ditampilkan segera. Namun, atribut ini hanya menekan warning pada level elemen tersebut; ia bukan mekanisme sinkronisasi data dan tidak memperbaiki penyebab perbedaan.

<time suppressHydrationWarning dateTime={iso}>
  {new Date(iso).toLocaleString()}
</time>

Jangan gunakan atribut ini pada container besar, daftar, formulir, atau komponen yang berbeda strukturnya antara server dan client. Warning yang disembunyikan dapat menutupi bug nyata, termasuk data yang salah, elemen interaktif yang berubah, atau markup yang tidak konsisten.

Kesalahan umum dan keputusan desain

  • Mengatur timezone server saja. Menyetel proses server ke UTC dapat membuat hasil server lebih konsisten, tetapi browser tetap memakai timezone pengguna. Ini bukan solusi jika formatter client masih memakai default.
  • Memformat ulang prop yang sama di client. Jika server sudah mengirim label final, jangan hitung ulang label pada render awal client kecuali locale pengguna memang menjadi kebutuhan produk.
  • Memakai Date.now() di JSX. Nilai tersebut tidak stabil. Kirim timestamp dari server, simpan sebagai state setelah mount, atau tampilkan placeholder.
  • Mengabaikan angka dan mata uang. Mismatch tidak hanya terjadi pada tanggal. Pemisah ribuan, posisi simbol mata uang, dan digit desimal juga bergantung pada locale.
  • Mencampur kebijakan bisnis dan preferensi pengguna. Tentukan apakah waktu harus mengikuti timezone event, timezone organisasi, atau timezone perangkat. Keputusan ini harus dibuat di model produk/API, bukan secara tidak sengaja oleh default runtime.

Checklist pengujian sebelum rilis

  1. Uji aplikasi dengan timezone server berbeda, minimal UTC dan timezone yang digunakan bisnis.
  2. Uji browser dengan locale berbeda seperti id-ID dan en-US, serta timezone berbeda seperti Asia/Jakarta dan America/New_York.
  3. Gunakan fitur emulasi locale dan timezone di browser atau konfigurasi test end-to-end. Pada Playwright, locale dan timezoneId dapat ditetapkan pada konfigurasi context/test.
  4. Periksa respons HTML awal dan DOM setelah hydration untuk halaman yang berisi tanggal, harga, angka, atau teks relatif.
  5. Jalankan build dan server production SSR, bukan hanya development server. Mode development dapat menampilkan warning lebih jelas, tetapi perilaku deployment tetap harus diverifikasi.
  6. Pastikan API mengirim format waktu yang tidak ambigu, idealnya ISO 8601 dengan offset untuk instant, atau field timezone terpisah untuk waktu lokal berbasis aturan bisnis.
  7. Tambahkan test untuk boundary penting: pergantian hari, perubahan tanggal akibat konversi timezone, dan periode daylight saving time bila pengguna atau data mencakup wilayah terkait.

Prinsip akhirnya sederhana: render awal harus deterministik. Jika server dan browser harus menampilkan nilai yang sama, kirim hasil format dari server atau gunakan opsi Intl yang eksplisit. Jika tampilan harus mengikuti perangkat pengguna, render fallback yang sama terlebih dahulu lalu lakukan personalisasi setelah mount.