Hydration error pada aplikasi penampil dokumen kontrak interaktif berbasis Server-Side Rendering (SSR) terjadi ketika struktur DOM yang dihasilkan Node.js tidak identik dengan pohon DOM awal di browser. Mismatch ini memicu peringatan runtime di framework seperti React atau Vue, merusak event listener klausul hukum, hingga memicu render ulang seluruh tree yang mematikan performa.

Root Cause: Perbedaan Sanitasi dan Parsing DOM SSR vs Client

Penyebab utama inkonsistensi rendering dokumen legal berbasis rich-text/Markdown terbagi menjadi tiga faktor struktural:

  • Parser Normalization Mismatch: Pustaka sanitasi server-side berbasis JSDOM atau regex parser menghasilkan entitas teks yang berbeda dibandingkan API bawaan browser (DOMParser). Contohnya, karakter void elements seperti tag <br> atau penutupan otomatis pada tag paragraf bersarang (<p><div>...</div></p>) akan dinormalisasi secara berbeda oleh browser engine.
  • Entitas Non-Breaking Space (&nbsp; vs \u00A0): Parser backend sering merender entitas teks literal &nbsp;, sedangkan browser parser langsung mengonversinya menjadi byte \u00A0 dalam DOM text node, menghasilkan text mismatch saat rekonsiliasi.
  • Interferensi Ekstensi Browser: Alat penerjemah, pemeriksa tata bahasa, atau password manager sering menyuntikkan node seperti <font>, class tambahan, atau atribut data-* ke dalam elemen teks hukum sebelum proses hidrasi selesai.

Sanitasi Deterministik dan Isolasi dangerouslySetInnerHTML

Untuk menghindari perbedaan output, sanitasi string kontrak harus mengacu pada satu konfigurasi deterministik. Gunakan isomorphic sanitizer (seperti isomorphic-dompurify) dan hilangkan entitas karakter whitespace yang berpotensi membelah token.

import DOMPurify from 'isomorphic-dompurify';

export function sanitizeContract(rawHtml: string): string {
  // Normalisasi token whitespace non-breaking ke standar single-space atau entitas eksplisit
  const normalized = rawHtml
    .replace(/\u00A0/g, ' ')
    .replace(/&nbsp;/gi, ' ');

  return DOMPurify.sanitize(normalized, {
    ALLOWED_TAGS: ['p', 'b', 'i', 'strong', 'em', 'ul', 'ol', 'li', 'span', 'br'],
    ALLOWED_ATTR: ['class', 'data-clause-id'],
    RETURN_TRUSTED_TYPE: false,
  });
}

Render kontainer kontrak di dalam elemen pembungkus terisolasi menggunakan flag supresi yang tepat untuk komponen yang terpapar ekstensi eksternal:

interface ContractViewerProps {
  htmlContent: string;
}

export function ContractClauseViewer({ htmlContent }: ContractViewerProps) {
  const cleanHtml = sanitizeContract(htmlContent);

  return (
    <div
      className="contract-content-body"
      dangerouslySetInnerHTML={{ __html: cleanHtml }}
      suppressHydrationWarning={true}
    />
  );
}
Catatan: Atribut suppressHydrationWarning hanya membungkam peringatan pada kedalaman satu level (atribut dan teks langsung). Elemen bersarang kompleks tetap harus deterministik sejak tahap sanitasi string.

Penundaan Reaktivitas Checkbox Klausul Tanpa Layout Shift

Fitur interaktif seperti checkbox persetujuan Non-Disclosure Agreement (NDA) sering memicu layout shift jika dimuat bersyarat via isMounted rendering. Terapkan strategi isolasi state di mana elemen dirender secara statis di SSR, namun status interaktivitas (enabled/disabled) dideferensiasi setelah hidrasi selesai.

import { useState, useEffect } from 'react';

interface ClauseAgreementProps {
  clauseId: string;
  onAgree: (id: string, agreed: boolean) => void;
}

export function ClauseAgreementCheckbox({ clauseId, onAgree }: ClauseAgreementProps) {
  const [hydrated, setHydrated] = useState(false);
  const [checked, setChecked] = useState(false);

  // ponytail: fallback direct browser check, upgrade jika ada global context hydration
  useEffect(() => {
    setHydrated(true);
  }, []);

  const handleChange = (e: React.ChangeEvent<HTMLInputElement>) => {
    setChecked(e.target.checked);
    onAgree(clauseId, e.target.checked);
  };

  return (
    <label className="clause-checkbox-wrapper" style={{ minHeight: '24px', display: 'flex' }}>
      <input
        type="checkbox"
        id={`checkbox-${clauseId}`}
        checked={checked}
        disabled={!hydrated}
        onChange={handleChange}
      />
      <span className="clause-checkbox-label">
        Saya membaca dan menyetujui seluruh klausul rahasia (NDA) di atas.
      </span>
    </label>
  );
}

Verifikasi dan Testing Hidrasi

Jalankan pemeriksaan lokal untuk memastikan pohon DOM konsisten sebelum masuk tahap deployment:

  1. Nonaktifkan JavaScript di DevTools: Periksa apakah dokumen kontrak dirender lengkap tanpa elemen kosong.
  2. Bandingkan Snapshot Hash: Hitung hash SHA-256 dari output SSR HTML dan bandingkan dengan document.querySelector('.contract-content-body').innerHTML saat runtime pertama browser.
  3. Audits Layout Shift (CLS): Pastikan penundaan hidrasi checkbox memiliki reserved container width/height via CSS untuk menjaga nilai Cumulative Layout Shift tetap 0.