Hydration error pada aplikasi React/Next.js terjadi ketika output HTML yang dihasilkan oleh server berbeda dengan pohon DOM yang dibuat oleh browser pada render pertama. Pada sistem Applicant Tracking System (ATS) modern yang melayani pelamar global, penyebab paling umum adalah pemformatan tanggal dan waktu wawancara kerja yang bergantung pada locale atau timezone lokal mesin.

Akar Masalah: Timezone Mismatch pada SSR

Server production umumnya berjalan pada timezone UTC (Coordinated Universal Time). Ketika server merender komponen jadwal wawancara menggunakan fungsi seperti new Date().toLocaleString(), string yang dihasilkan menggunakan representasi UTC atau konfigurasi default server.

Ketika payload HTML tiba di peramban pelamar di Jakarta (WIB/UTC+7), engine React mengeksekusi hydration pass. Browser mengevaluasi kode yang sama menggunakan timezone lokal pengguna. Perbedaan string waktu antara server dan client memicu runtime error:

Error: Text content does not match server-rendered HTML.
Warning: Text content did not match. Server: "10/24/2024, 02:00 PM" Client: "10/24/2024, 09:00 PM"

Kode Masalah: Rendering Jadwal Langsung

Contoh implementasi rentan error pada formulir konfirmasi jadwal wawancara kerja:

// components/InterviewSchedule.tsx
// MASALAH: Menyebabkan hydration mismatch
export default function InterviewSchedule({ isoDate }: { isoDate: string }) {
  const formattedDate = new Date(isoDate).toLocaleString();

  return (
    <div className="schedule-slot">
      <label>Jadwal Wawancara Terpilih:</label>
      <span>{formattedDate}</span>
    </div>
  );
}

Solusi 1: Pemformatan Waktu Deterministik dengan Intl API

Cara paling efisien tanpa menunda rendering adalah mengunci timezone target secara eksplisit pada SSR dan client menggunakan Intl.DateTimeFormat. Ini berguna jika jadwal wawancara harus selalu ditampilkan berdasarkan zona waktu kantor perekrut.

// components/DeterministicSchedule.tsx
interface Props {
  isoDate: string;
  timeZone?: string;
}

export default function DeterministicSchedule({
  isoDate,
  timeZone = "Asia/Jakarta"
}: Props) {
  const formattedDate = new Intl.DateTimeFormat("id-ID", {
    dateStyle: "full",
    timeStyle: "short",
    timeZone,
  }).format(new Date(isoDate));

  return (
    <div className="schedule-box">
      <p>Waktu Wawancara: {formattedDate} ({timeZone})</p>
    </div>
  );
}

Solusi 2: Pola Two-Pass Rendering dengan useEffect

Jika aplikasi ATS wajib menampilkan waktu berdasarkan zona waktu lokal pelamar, gunakan strategi two-pass rendering. Server merender fallback placeholder statis, kemudian komponen memperbarui nilainya di client setelah proses mounting selesai.

// components/ClientAwareSchedule.tsx
"use client";

import { useState, useEffect } from "react";

export default function ClientAwareSchedule({ isoDate }: { isoDate: string }) {
  const [clientTime, setClientTime] = useState<string | null>(null);

  useEffect(() => {
    // Dijalankan hanya di client setelah hydration
    setClientTime(new Date(isoDate).toLocaleString());
  }, [isoDate]);

  return (
    <div className="slot-confirm">
      <span>Jadwal Wawancara (Waktu Lokal):</span>
      <span>{clientTime ?? "Memuat waktu lokal..."}</span>
    </div>
  );
}

Solusi 3: Isolasi Komponen via Dynamic Import (Next.js)

Untuk komponen formulir yang kompleks dan tidak krusial terhadap Search Engine Optimization (SEO), isolasi elemen tersebut dengan mematikan server-side rendering pada tingkat komponen via next/dynamic.

// app/apply/page.tsx
import dynamic from "next/dynamic";

const LocalizedDatePicker = dynamic(
  () => import("@/components/LocalizedDatePicker"),
  { ssr: false, loading: () => <p>Memuat kalender...</p> }
);

export default function JobApplicationPage() {
  return (
    <form>
      <h2>Konfirmasi Slot Wawancara</h2>
      <LocalizedDatePicker />
      <button type="submit">Kirim Lamaran</button>
    </form>
  );
}

Ringkasan Trade-Off Solusi

  • Deterministik (Intl API): Performa render tercepat, aman SEO, namun memerlukan passing data timezone referensi secara eksplisit.
  • Two-Pass Rendering: Menghilangkan hydration mismatch untuk waktu lokal pengguna, tetapi memicu render tambahan (layout shift kecil).
  • Dynamic Client-Only: Solusi definitif untuk komponen interaktif berat, namun menonaktifkan SSR pada elemen tersebut.