Hydration mismatch terjadi ketika markup server-rendered tidak sinkron dengan state di sisi klien, dan kelemahan ini bisa menimbulkan pengalaman buruk, terutama bagi pengguna dengan preferensi input tidak standar seperti pengguna kidal. Mengacu pada a bug which only affected left-handed users, ketidakkonsistenan fokus, class dinamis, atau shadow DOM dapat membuat elemen yang tampak serupa pada SSR menjadi perilaku berbeda saat berinteraksi di sisi klien.

Memahami 'Bug Kidal' dan Bahaya Render Mismatch

Cerita bug yang hanya memengaruhi pengguna kidal menunjukkan bagaimana asumsi posisi interaksi (misalnya tombol navigasi di sisi kanan) bisa melewati QA konvensional. Versi server mengatur order visual berdasarkan layout default, namun saat app di-hydrate, logika client mengubah posisi fokus, mengandalkan event listener di sisi kanan. Perbedaan ini menciptakan mirror UI yang tampak sama secara visual tetapi sensitif terhadap arah input: satu input (kanan) aktif di server, satu lagi (kiri) aktif di client.

Hydration mismatch serupa sering muncul ketika state fokus, attribute `class`, atau atribut-tergantung-heap berubah saat client mengambil alih, menyebabkan perbedaan interaksi bagi pengguna yang mengandalkan kebiasaan pengerjaan tertentu.

Sumber Ketidakkonsistenan dan Mengapa Mereka Penting

Fokus dan Posisi Default

SSR menetapkan sequential order atau tabindex tertentu berdasarkan layout default (misalnya tombol utama di kanan bawah). Ketika client merender ulang dengan logika kontekstual (misalnya layout berubah untuk mode kidal), elemen yang residu di DOM tetap memiliki atribut fokus lama, sehingga interaksi keyboard/touch menjadi unpredictable.

Shadow DOM dan Element Tersembunyi

Komponen seperti Web Component dengan Shadow DOM mungkin menyembunyikan elemen aktif. SSR hanya menghasilkan markup host, tapi saat klien memperluas shadow DOM, event bubbling atau focus order bisa berbeda, tanpa perbedaan HTML yang terlihat. Ini rawan terjadi pada UI yang memindah-mindahkan kontrol antara sisi kiri-kanan.

Class Dinamis dan Render Branching

Class yang dinamis (misalnya toggled berdasarkan preferensi `isLeftHanded`) bisa membuat struktur DOM SSR berbeda dari client karena data awal yang berbeda atau kondisi async. Jika SSR menambahkan `class="right-handed"` tapi client merender `class="left-handed"` setelah fetch preferensi, hydration mismatch muncul.

Debugging Hydration untuk Pengguna Kidal

Gunakan pendekatan berikut agar perbedaan terlihat sebelum menjadi bug produksi:

  1. Snapshot DOM: Ambil snapshot SSR (HTML) dan bandingkan dengan snapshot hasil hydrating client. Fokus pada atribut fokus, class, dan node order.
  2. Konsistensi State: Pastikan state awal yang dipakai server dan client sama. Simulasi preferensi pengguna kidal saat rendering server (misalnya dengan query param atau cookie). Gunakan logging untuk state initial hydration.
  3. Menandai DOM Sensitif: Pasang atribut data- seperti data-hydration-sensitive pada container kontrol kiri-kanan. Gunakan devtools untuk membandingkan atribut ini setelah hydrate.
  4. Testcase Khusus: Buat test yang mengirimkan event input di sisi kiri dan kanan untuk memastikan focus order tetap konsisten. Tools seperti Playwright dapat menjalankan scenario ini dengan parameter `isLeftHanded`.

Contoh Pengujian Otomatis

test('tombol navigasi tetap konsisten untuk pengguna kidal', async ({ page }) => {
  await page.goto('/dashboard?handed=left');
  const tombolKiri = await page.getByRole('button', { name: 'Navigasi kiri' });
  await tombolKiri.focus();
  await expect(tombolKiri).toBeFocused();
  // Pastikan hydration tidak mengubah tabindex
  await page.reload();
  await expect(tombolKiri).toBeFocused();
});

Arsitektur agar Hydration Deterministik

Strategi berikut menekan perbedaan antara SSR dan client:

  • Shared Initial State: Pastikan data/props awal yang digunakan client sama persis dengan data yang di-render di server. Serialisasi state pada HTML dan deserialisasi di client.
  • Prefer Helper untuk Layout Khusus: Alihkan logika layout khusus kidal ke CSS (misalnya body.rtl atau .mirror-left) sehingga markup tidak perlu berubah antar render.
  • Shadow DOM Awareness: Jika menggunakan Web Components, expose API untuk men-set preferensi secara imperatif sebelum hydration selesai, agar atribut fokus tetap predictable.
  • Hydration Boundary Terkontrol: Batasi area hidrasi pada komponen yang benar-benar perlu interaksi dinamis. Komponen statis sisakan hanya markup SSR sehingga tidak ada mismatch.

Checklist Mitigasi

  • ✅ Serialisasi state preferensi pengguna (termasuk kidal) dalam markup SSR.
  • ✅ Tandai elemen yang sensitif terhadap urutan fokus dengan data-hydration-sensitive.
  • ✅ Terapkan test-case input sisi kiri/k kanan menggunakan automation (Playwright/Puppeteer).
  • ✅ Hindari memodifikasi class/fokus utama setelah hydration tanpa memeriksa snapshot struktur DOM.
  • ✅ Review shadow DOM untuk memastikan event bubbling tetap searah.

Kode Minimal Penanda Fokus Konsisten

Berikut contoh React sederhana yang menyelaraskan atribut fokus tergantung preferensi dhyid:

const initialHanded = window.__INITIAL_HAND || 'right';
const FocusableButton = ({ label }) => {
  const direction = useMemo(() => (initialHanded === 'left' ? 'row-reverse' : 'row'), [initialHanded]);
  return (
    <button
      data-hydration-sensitive="focus-order"
      style={{ flexDirection: direction }}
    >
      {label}
    </button>
  );
};

Serialisasi __INITIAL_HAND di HTML SSR agar client dapat menggunakan nilai yang sama saat hydrate.

Penutup

Hydration mismatch mungkin jarang terlihat, tetapi ketika memengaruhi pengguna dengan preferensi non-konvensional seperti kidal, dampaknya terasa nyata. Dengan pendekatan deterministic—shared state, CSS untuk layout, debug sensitif, dan test khusus—render tetap konsisten tanpa membuat ulang seluruh UI. Pastikan setiap perubahan layout juga melewati snapshot SSR/client dan test case arah input baru.