Hydration error sering muncul pada Next.js App Router ketika memformat tanggal menggunakan Intl.DateTimeFormat atau Date.prototype.toLocaleDateString(). Masalah ini berakar pada perbedaan representasi waktu antara lingkungan eksekusi server dan browser.

Akar Masalah: Reconciler React dan Timezone Host

Server Node.js, container Docker, atau serverless edge runtime umumnya berjalan pada konfigurasi sistem berstandar UTC (Universal Time Coordinated). Sebaliknya, browser pengguna mengeksekusi kode JavaScript berdasarkan timezone lokal sistem operasi (misalnya Asia/Jakarta atau UTC+7).

Ketika komponen dieksekusi di server, string tanggal dihasilkan berdasarkan offset UTC. Saat fase hydration, reconciler React merender ulang Virtual DOM di client menggunakan timezone lokal perangkat. React membandingkan tree DOM hasil Server-Side Rendering (SSR) dengan hasil render awal di client:

// Contoh output string waktu: 2025-01-01T22:30:00Z
Server (UTC):     "01/01/2025"
Client (UTC+7):   "02/01/2025"

Perbedaan output string tersebut melanggar aturan determinisme render React. Reconciler melempar peringatan: Text content does not match server-rendered HTML.

Trade-off Solusi Umum

Terdapat dua pendekatan cepat yang sering diambil pengembang, namun keduanya membawa konsekuensi performa dan integritas UI.

1. Menggunakan suppressHydrationWarning

Menambahkan atribut suppressHydrationWarning pada tag pembungkus membungkam peringatan reconciler React pada level node tersebut.

  • Kelebihan: Menghilangkan error di konsol tanpa menambah overhead eksekusi kode.
  • Risiko: DOM tidak di-update otomatis oleh React pasca-hydration. Pengguna membaca tanggal server (UTC) sampai state komponen berubah atau re-render terpicu. Jika diletakkan di root atau wrapper generik, atribut ini dapat menyamarkan bug hydration struktural lainnya.

2. Penundaan Render via useEffect (Client-Only Mounting)

Komponen menampilkan skeleton atau placeholder saat SSR, lalu merender tanggal lokal setelah browser selesai melakukan mounting (useEffect).

  • Kelebihan: Menghindari tabrakan teks saat hydration reconciler berjalan.
  • Risiko: Memicu Cumulative Layout Shift (CLS) dan Flash of Unstyled Content (FOUC). Skor Core Web Vitals memburuk karena pergeseran elemen visual secara tiba-tiba di viewport pembaca.

Solusi 1: Sinkronisasi Timezone Isomorphic via Cookie (Rekomendasi SSR)

Pendekatan terbaik untuk Server Component adalah meneruskan timezone pengguna ke server melalui cookie HTTP. Dengan cara ini, server dapat merender output tanggal dengan timezone yang sama persis seperti browser.

Langkah 1: Tangkap Timezone di Client

Letakkan script ringan di root layout untuk menyimpan IANA timezone pengguna ke cookie jika belum terdefinisi.

// app/layout.tsx
export default function RootLayout({ children }: { children: React.ReactNode }) {
  return (
    <html lang="id">
      <head>
        <script
          dangerouslySetInnerHTML={{
            __html: `
              (function() {
                const tz = Intl.DateTimeFormat().resolvedOptions().timeZone;
                if (document.cookie.indexOf('user_tz=') === -1) {
                  document.cookie = 'user_tz=' + encodeURIComponent(tz) + '; path=/; max-age=31536000; SameSite=Lax';
                }
              })();
            `,
          }}
        />
      </head>
      <body>{children}</body>
    </html>
  );
}

Langkah 2: Baca Timezone di Server Component

Gunakan helper cookies() dari Next.js untuk memformat tanggal secara konsisten di server.

// components/ServerFormattedDate.tsx
import { cookies } from 'next/headers';

interface Props {
  date: Date | string;
}

export async function ServerFormattedDate({ date }: Props) {
  const cookieStore = await cookies();
  const timeZone = cookieStore.get('user_tz')?.value || 'UTC';
  const targetDate = typeof date === 'string' ? new Date(date) : date;

  const formatted = new Intl.DateTimeFormat('id-ID', {
    timeZone,
    dateStyle: 'medium',
    timeStyle: 'short',
  }).format(targetDate);

  return <time dateTime={targetDate.toISOString()}>{formatted}</time>;
}

Solusi 2: Komponen Isolasi dengan Semantic Tag <time> (Bebas CLS)

Jika routing bersifat Static Site Generation (SSG) atau tidak memungkinkan pembacaan cookie server, gunakan Client Component yang dirancang khusus untuk meminimalkan pergeseran layout.

// components/FormattedDate.tsx
'use client';

import { useState, useEffect } from 'react';

interface Props {
  date: Date | string;
}

export function FormattedDate({ date }: Props) {
  const targetDate = typeof date === 'string' ? new Date(date) : date;
  const isoString = targetDate.toISOString();
  
  // ponytail: fallback statis UTC mempertahankan konsistensi render SSR server-side
  const [displayDate, setDisplayDate] = useState<string>(() => {
    return new Intl.DateTimeFormat('id-ID', {
      timeZone: 'UTC',
      dateStyle: 'medium',
    }).format(targetDate);
  });

  useEffect(() => {
    const localTz = Intl.DateTimeFormat().resolvedOptions().timeZone;
    const localized = new Intl.DateTimeFormat('id-ID', {
      timeZone: localTz,
      dateStyle: 'medium',
    }).format(targetDate);
    
    setDisplayDate(localized);
  }, [targetDate]);

  return (
    <time dateTime={isoString} suppressHydrationWarning>
      {displayDate}
    </time>
  );
}

Catatan: Atribut suppressHydrationWarning aman digunakan di sini karena hanya diisolasi pada tag semantik <time>, bukan pada parent node container, dan nilainya langsung disinkronkan ke local timezone di dalam useEffect.

Kesimpulan Pemilihan Solusi

  • Pilih Pendekatan Cookie (Isomorphic SSR): Jika aplikasi berbasis dynamic rendering, mementingkan SEO penuh tanpa jeda rendering client, dan membutuhkan metrik CLS 0 mutlak.
  • Pilih Komponen Isolasi <time>: Jika halaman bersifat static (SSG/ISR) di mana server tidak membaca header request saat runtime.