Akar Masalah Hydration Error dari Kebiasaan Vibecoding

Pola vibecoding atau pantsing—menulis kode secara cepat mengandalkan AI scaffolding atau insting tanpa mendesain kontrak state—kerap memicu masalah mendasar pada framework berbasis Server-Side Rendering (SSR) seperti Next.js. Masalah paling umum adalah Hydration Mismatch Error (misalnya React Error #418 atau #423).

Hydration mismatch terjadi ketika tree Virtual DOM yang dihasilkan oleh server render tidak identik dengan tree yang dibangun oleh browser saat eksekusi JavaScript client-side pertama kali. Pola vibecoding cenderung menghasilkan komponen yang mengakses API browser secara implisit di siklus render awal:

  • Membaca langsung objek global seperti window, document, atau localStorage saat deklarasi state.
  • Menggunakan generator nilai non-deterministik seperti Date.now(), new Date().toLocaleTimeString(), atau Math.random() di root komponen.
  • Asumsi data parsial: server merender kondisi fallback kosong, sedangkan client langsung merender data dari persistent storage tanpa sinkronisasi fase render.

Kasus Nyata: Mismatch Akibat Direct Browser Storage Access

Kode hasil prompt instan sering kali mencoba membaca preferensi tema atau auth state secara sinkron langsung dari localStorage:

// CONTOH SALAH: Membaca localStorage langsung saat render
export default function UserBadge() {
  const token = typeof window !== 'undefined' 
    ? localStorage.getItem('auth_token') 
    : null;

  return (
    <div className="badge">
      {token ? <span>Authenticated</span> : <span>Guest</span>}
    </div>
  );
}

Mekanisme kegagalannya:

  1. Server Run: Node.js mengeksekusi fungsi. window tidak terdefinisi. Node menghasilkan tag <span>Guest</span> ke dalam payload HTML.
  2. Browser Run: HTML diunduh. Runtime JavaScript React mulai melakukan rekonsiliasi (hydration). Di client, window tersedia dan localStorage.getItem('auth_token') mengembalikan string token.
  3. Mismatch: Client mengharapkan DOM berisi <span>Authenticated</span>, tetapi server mengirim <span>Guest</span>. React menghentikan proses hydration parsial dan mencatat hydration error di konsol.

Solusi 1: Isolasi Client-Only Mount via Custom Hook

Solusi deterministik pertama adalah menunda evaluasi state browser sampai fase mount client selesai. Gunakan hook sederhana tanpa dependensi tambahan:

import { useState, useEffect } from 'react';

export function useIsMounted() {
  const [mounted, setMounted] = useState(false);

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

  return mounted;
}

Implementasi pada komponen target:

import { useState, useEffect } from 'react';
import { useIsMounted } from './useIsMounted';

export default function UserBadge() {
  const isMounted = useIsMounted();
  const [token, setToken] = useState(null);

  useEffect(() => {
    setToken(localStorage.getItem('auth_token'));
  }, []);

  // Render konsisten antara SSR dan initial client render
  if (!isMounted) {
    return <div className="badge"><span>Guest</span></div>;
  }

  return (
    <div className="badge">
      {token ? <span>Authenticated</span> : <span>Guest</span>}
    </div>
  );
}
Catatan Teknis: Pendekatan ini dapat memicu layout shift (CLS) kecil jika fallback visual berbeda secara drastis dari hasil akhir client. Gunakan skeleton wrapper dengan dimensi layout yang identik untuk mengatasinya.

Solusi 2: Sinkronisasi State via useSyncExternalStore

Untuk state reaktif yang bersumber dari luar runtime React (seperti browser API atau storage), React 18+ menyediakan useSyncExternalStore yang secara eksplisit menerima snapshot server dan client terpisah:

import { useSyncExternalStore } from 'react';

function subscribe(callback) {
  window.addEventListener('storage', callback);
  return () => window.removeEventListener('storage', callback);
}

export function useStorageValue(key) {
  return useSyncExternalStore(
    subscribe,
    () => localStorage.getItem(key), // Snapshot client
    () => null                       // Snapshot server (deterministik)
  );
}

Solusi 3: Menegakkan Skema Validasi Deterministik

Data dinamis seperti timestamp harus diformat menggunakan parser dengan zona waktu dan locale yang didefinisikan secara eksplisit, bukan mengandalkan locale bawaan mesin runtime pengembang.

// SALAH: Mengandalkan locale default runtime mesin
// const formatted = new Date().toLocaleString();

// BENAR: Kontrak format deterministik UTC/ISO standar
export function formatTimestamp(dateString) {
  const date = new Date(dateString);
  return new Intl.DateTimeFormat('id-ID', {
    dateStyle: 'medium',
    timeStyle: 'short',
    timeZone: 'Asia/Jakarta',
  }).format(date);
}

Pencegahan: Aturan Linter untuk Menjaga Kontrak SSR

Untuk menghentikan regresi kode akibat scaffolding cepat tanpa review struktur, terapkan aturan linting berikut di konfigurasi ESLint (.eslintrc.json):

{
  "plugins": ["react-hooks"],
  "rules": {
    "no-restricted-globals": [
      "error",
      {
        "name": "window",
        "message": "Akses objek 'window' langsung memicu hydration mismatch pada SSR. Gunakan hook useMounted atau jalankan di dalam useEffect."
      },
      {
        "name": "localStorage",
        "message": "Akses 'localStorage' sinkron pada render phase memicu mismatch. Gunakan useSyncExternalStore atau isolasi client-only."
      }
    ]
  }
}

Mengandalkan AI scaffolding diperbolehkan untuk akselerasi prototyping, namun pemahaman execution boundary antara server dan client tetap menjadi tanggung jawab engineer dalam menjaga integritas DOM tree.