Hydration error terjadi ketika Document Object Model (DOM) hasil render server tidak identik dengan Virtual DOM yang dihasilkan React selama fase initial render di browser. Masalah ini kerap muncul saat mengintegrasikan output script CGI era 90-an—seperti skrip Perl dari Matt's Script Archive (Count.cgi, Guestbook, atau WWWBoard)—ke dalam aplikasi modern berbasis Next.js atau React SSR.

Script CGI lawas umumnya beroperasi dengan memanggil document.write() atau langsung menyuntikkan node HTML (misalnya tag <table>, <font>, atau <img>) ke DOM sebelum engine JavaScript modern menyelesaikan proses hidrasi. Perubahan struktur DOM yang tidak terlacak ini memicu React memunculkan error seperti Hydration failed because the initial UI does not match what was rendered on the server.

Mekanisme Masalah: Mutasi DOM Sebelum Rehidrasi

Saat Server-Side Rendering (SSR) berjalan, React merender komponen menjadi string HTML statis. Browser menerima string ini, membangun tree DOM awal, lalu mengeksekusi JavaScript runtime framework untuk mencocokkan node yang ada (rehidrasi).

Jika halaman menyertakan tag script legacy seperti berikut:

<!-- Script counter lawas ala CGI Perl -->
<script src="/cgi-bin/counter.cgi?page=home"></script>

Script tersebut langsung menyisipkan elemen sebelum bundle React dieksekusi:

// Perilaku umum script CGI lama
document.write('<img src="/images/digits/1.gif"><img src="/images/digits/2.gif">');
// atau memodifikasi parentNode langsung:
// document.getElementById('visitor-count').innerHTML = 'Total: 12';

Ketika React memulai hidrasi pada node target, ditemukan ketidaksesuaian child nodes antara DOM fisik dan representasi VDOM server. React memutus proses rekonsiliasi dan memaksa render ulang sisi klien (client bailout), menyebabkan glitch visual (layout shift) dan kehilangan state input.

Solusi 1: Isolasi Siklus Hidup dengan useEffect

Pola paling sederhana adalah mencegah script CGI dieksekusi di server dan menunda injeksinya hingga hidrasi React selesai seutuhnya. Gunakan hook useEffect untuk memuat elemen script secara imperatif.

'use strict';
import { useEffect, useRef } from 'react';

export function LegacyCgiCounter({ pageId }) {
  const containerRef = useRef(null);

  useEffect(() => {
    const container = containerRef.current;
    if (!container) return;

    // Buat elemen script secara dinamis pasca-hidrasi
    const script = document.createElement('script');
    script.src = `/cgi-bin/counter.cgi?page=${encodeURIComponent(pageId)}`;
    script.async = true;

    container.appendChild(script);

    return () => {
      // Cleanup saat unmount
      container.innerHTML = '';
    };
  }, [pageId]);

  // Render container kosong yang konsisten antara server dan client
  return <div ref={containerRef} suppressHydrationWarning />;
}

Atribut suppressHydrationWarning dipertahankan pada kontainer jika skrip memanipulasi atribut langsung, tetapi penundaan via useEffect memastikan hidrasi internal React sudah selesai sebelum mutasi terjadi.

Solusi 2: Enkapsulasi Total Menggunakan Shadow DOM

Jika script CGI menyisipkan style global tak terduga atau memanggil selector seperti document.querySelector yang berpotensi merusak UI framework, isolasikan tree DOM tersebut menggunakan Shadow Root.

import { useEffect, useRef } from 'react';

export function IsolatedLegacyWidget({ scriptUrl }) {
  const hostRef = useRef(null);

  useEffect(() => {
    if (!hostRef.current) return;

    // Inisialisasi Shadow Root tertutup dari penetrasi querySelector biasa
    const shadow = hostRef.current.attachShadow({ mode: 'open' });
    
    // Buat target injeksi khusus di dalam shadow root
    const mountPoint = document.createElement('div');
    shadow.appendChild(mountPoint);

    const script = document.createElement('script');
    script.src = scriptUrl;
    shadow.appendChild(script);

    return () => {
      hostRef.current.innerHTML = '';
    };
  }, [scriptUrl]);

  return <div ref={hostRef}></div>;
}

Shadow DOM mencegah script eksternal membocorkan aturan CSS ke komponen modern di sekitarnya dan membatasi manipulasi DOM hanya dalam shadow tree.

Solusi 3: Iframe Sandboxing untuk Script Berbahaya (document.write)

Jika script CGI lawas menggunakan document.write(), pendekatan useEffect standar tetap akan gagal karena browser modern memblokir atau menghapus isi seluruh halaman ketika document.write() dipanggil setelah parsing dokumen selesai. Iframe adalah satu-satunya metode aman untuk mengeksekusi skrip kategori ini.

export function SandboxedCgiWidget({ cgiUrl, title }) {
  return (
    <iframe
      title={title}
      srcDoc={`<!DOCTYPE html><html><head></head><body><script src="${cgiUrl}"><\/script></body></html>`}
      sandbox="allow-scripts allow-same-origin"
      loading="lazy"
      style={{ border: 'none', width: '100%', minHeight: '80px', overflow: 'hidden' }}
    />
  );
}

Panduan Memilih Solusi

  • useEffect Hook: Pilih opsi ini jika script aman, tidak menggunakan document.write(), dan hanya menyisipkan elemen sederhana. Solusi ini paling hemat resource.
  • Shadow DOM: Pilih opsi ini jika script memuat style CSS bawaan era 90-an yang berpotensi merusak cascade layout sistem modern.
  • Iframe Sandbox: Gunakan opsi ini wajib jika script CGI mengandalkan document.write() atau manipulasi window global yang berisiko merusak state JavaScript aplikasi React.