Hydration error pada aplikasi berbasis SSR (Server-Side Rendering) seperti Next.js sering muncul bukan karena bug pada kode aplikasi, melainkan intervensi skrip eksternal. Script browser automation (Playwright, Puppeteer, Selenium) yang menginjeksi atribut penanda atau memanipulasi node DOM sebelum proses hidrasi selesai akan merusak rekonsiliasi antara virtual DOM dan DOM asli.

Akar Masalah: Checksum dan Deterministic Reconciliation

Saat server merender HTML, React menghasilkan representasi DOM deterministik. Pada browser, React me-mount virtual DOM dan membandingkannya dengan node HTML aktual untuk memasang event listener (reconciliation). Jika skrip otomatisasi menyuntikkan node, atribut tambahan, atau menghapus elemen sebelum runtime React mengeksekusi hydrateRoot, checksum tree menjadi tidak valid. Browser melempar error: Hydration failed because the initial UI does not match what was rendered on the server.

Contoh Kegagalan Injeksi Playwright / Puppeteer

Kesalahan umum terjadi ketika automation script menyuntikkan helper atribut ke DOM via page.evaluate() atau page.addInitScript() saat dokumen masih dalam status interaktif:

// CONTOH SALAH: Injeksi atribut langsung saat inisialisasi dokumen
await page.addInitScript(() => {
  // Memodifikasi elemen DOM SSR sebelum hidrasi selesai
  const target = document.querySelector('#submit-button');
  if (target) {
    target.setAttribute('data-automation-ready', 'true');
  }
});

Operasi mutasi di atas mengubah atribut elemen langsung pada markup SSR, memicu mismatch seketika saat hidrasi React berjalan di klien.

Metode Debugging Mismatch

  1. Isolasi Environment: Jalankan browser automation tanpa headless mode dan aktifkan toggle pause debugger sebelum skrip interaksi dieksekusi. Periksa apakah error tetap muncul.
  2. Bandingkan Snapshot DOM: Cetak document.documentElement.outerHTML sebelum dan sesudah script automation berjalan untuk melihat mutasi tag atau atribut liar.
  3. Cek Mutasi Browser Extensions / Scraper Injections: Pastikan engine automation tidak menyuntikkan content script yang menambah ID acak pada wrapper <body> atau <main>.

Solusi Praktis dan Implementasi

1. Menunda Mutasi Menggunakan MutationObserver Pasca-Hidrasi

Jika automation script wajib menambahkan custom overlay, logger, atau listener pada node tertentu, tunda mutasi hingga node terhidrasi sepenuhnya, atau pantau status mount komponen:

// Solusi di sisi script Playwright: Tunggu indikator siap sebelum mutasi
await page.waitForFunction(() => {
  return document.readyState === 'complete' && window.__NEXT_DATA__ !== undefined;
});

await page.evaluate(() => {
  const observer = new MutationObserver((mutations, obs) => {
    const target = document.querySelector('[data-testid="target-btn"]');
    if (target) {
      target.setAttribute('data-automation-ready', 'true');
      obs.disconnect();
    }
  });

  observer.observe(document.body, { childList: true, subtree: true });
});

2. Gunakan Data Attribute Deterministik di Sisi Komponen

Hindari injeksi atribut manual via scripting DOM API. Tambahkan atribut pengujian langsung pada JSX agar server dan client selalu sinkron:

// Komponen React / Next.js
export default function SubmitButton({ isLoading }) {
  return (
    <button
      type="submit"
      data-testid="checkout-submit"
      data-test-state={isLoading ? 'loading' : 'ready'}
    >
      Bayar Sekarang
    </button
  );
}

Dengan mendefinisikan data-testid dan state dalam render pass JSX, React memperhitungkan atribut tersebut dalam kalkulasi hidrasi, sehingga tidak ada perbedaan virtual tree.

3. Eksekusi Script Otomasi pada Lifecycle Event yang Tepat

Gunakan opsi navigasi yang menunggu seluruh lifecycle jaringan dan script selesai, misalnya status load:

// Playwright: Pastikan lifecycle state 'load' tercapai sebelum manipulasi
await page.goto('https://example.com/checkout', {
  waitUntil: 'load'
});

// Target elemen spesifik menggunakan selector data-testid bawaan
const submitBtn = page.locator('[data-testid="checkout-submit"]');
await submitBtn.waitFor({ state: 'attached' });
await submitBtn.click();

Kesimpulan

Hydration error akibat script otomatisasi browser terjadi akibat intervensi DOM prematur pada fase reconciliation React. Solusi definitifnya adalah memindahkan atribut pengujian ke dalam kode komponen (deterministik), mengandalkan selector statis bawaan, dan menunda modifikasi DOM eksternal menggunakan MutationObserver atau event load.