Akar Masalah: Mengapa Hydration Mismatch Terjadi di Django SSR

Saat mengintegrasikan island architecture atau komponen interaktif modern (seperti React 18+) ke dalam template server-side rendering (SSR) Django, salah satu kendala utama yang sering muncul di konsol peramban adalah Hydration Mismatch (React Error #418 atau #423).

Hydration mismatch terjadi ketika DOM tree hasil render pertama di sisi klien (client-side render) tidak identik dengan struktur HTML yang dihasilkan oleh template engine Django. Dalam konteks form dan otentikasi Django, penyebab utamanya adalah tag template {% csrf_token %}. Tag ini secara otomatis menyuntikkan elemen HTML tersembunyi:

<input type="hidden" name="csrfmiddlewaretoken" value="dK8j9F...">

Jika React mencoba melakukan hidrasi (hydrateRoot) langsung di atas elemen pembungkus form SSR tersebut tanpa sinkronisasi nilai token secara deterministik sebelum kompilasi pohon virtual DOM klien, rekonsiliasi DOM akan gagal. React mendeteksi ketidakcocokan atribut value atau ketidakhadiran node <input> di virtual DOM awal klien. Akibatnya, React membuang node SSR yang ada dan melakukan de-opt ke client-side render penuh, merusak performa First Contentful Paint (FCP) dan berpotensi menghilangkan event listener bawaan.

Pola Arsitektur: Isolasi Mount Boundary

Kesalahan umum dalam integrasi Django-React adalah mencoba menghidrasi seluruh elemen <form> yang di-render oleh Django secara langsung sebagai akar hidrasi. Pendekatan yang lebih aman dan terprediksi adalah memisahkan mount boundary antara markup statis Django dan komponen interaktif klien.

Pilihan Arsitektur Mount

  1. Island Component via createRoot (CSR Island): Jika markup di dalam form tidak membutuhkan pengindeksan SEO kritis, jangan gunakan hydrateRoot. Gunakan createRoot pada kontainer kosong di dalam template Django, lalu render input CSRF langsung dari state React.
  2. True Hydration via hydrateRoot: Jika elemen SSR harus dipertahankan untuk SEO atau Largest Contentful Paint (LCP), virtual DOM awal React wajib menghasilkan markup dan token CSRF yang 100% identik dengan hasil parsing template Django.

Strategi Transfer Token CSRF Deterministik

Agar hidrasi deterministik berhasil, klien harus mengakses token CSRF yang persis sama dengan yang dihasilkan Django pada siklus request yang bersangkutan sebelum pohon React di-render.

Metode 1: Ekstraksi Meta Tag

Pendekatan ini menyuntikkan token langsung ke dalam tag <meta> di <head> template Django. Klien membaca token ini secara sinkron sebelum memanggil hydrateRoot.

<!-- base.html Django -->
<head>
    <meta name="csrf-token" content="{{ csrf_token }}">
</head>

Metode 2: Django json_script

Filter json_script Django mengamankan token dari potensi injeksi skrip berbahaya (XSS) dan menyimpannya di DOM sebagai skrip JSON statis yang dapat diparsing langsung oleh JavaScript.

<!-- template.html Django -->
{{ csrf_token|json_script:"django-csrf-token" }}

Metode 3: Pembacaan Cookie Bawaan Django

Secara default, middleware Django (django.middleware.csrf.CsrfViewMiddleware) mengatur cookie bernama csrftoken (selama CSRF_COOKIE_HTTPONLY = False). Klien dapat mengekstrak nilai ini tanpa modifikasi markup.

Implementasi Praktis

Berikut adalah implementasi end-to-end yang menjamin hidrasi berjalan tanpa peringatan mismatch.

1. Template Django (templates/order_form.html)

{% load static %}
<!DOCTYPE html>
<html lang="id">
<head>
    <meta charset="UTF-8">
    <meta name="csrf-token" content="{{ csrf_token }}">
    <title>Checkout Order</title>
</head>
<body>
    <!-- Mount Boundary Khusus -->
    <div id="order-app-mount">
        <form method="post" action="/api/orders/">
            <input type="hidden" name="csrfmiddlewaretoken" value="{{ csrf_token }}">
            <div class="field-group">
                <label for="item_name">Nama Item</label>
                <input id="item_name" name="item_name" type="text" value="">
            </div>
            <button type="submit">Proses Pembayaran</button>
        </form>
    </div>

    <script src="{% static 'bundle.js' %}" defer></script>
</body>
</html>

2. React Mount Point (src/index.tsx)

Pada sisi frontend, kita membaca token secara sinkron sebelum menjalankan rekonsiliasi hidrasi. Pastikan output JSX awal benar-benar cocok dengan HTML yang di-render Django di dalam kontainer.

import React from 'react';
import { hydrateRoot } from 'react-dom/client';

interface OrderFormProps {
  csrfToken: string;
}

function OrderForm({ csrfToken }: OrderFormProps) {
  return (
    <form method="post" action="/api/orders/">
      <input type="hidden" name="csrfmiddlewaretoken" value={csrfToken} />
      <div className="field-group">
        <label htmlFor="item_name">Nama Item</label>
        <input id="item_name" name="item_name" type="text" defaultValue="" />
      </div>
      <button type="submit">Proses Pembayaran</button>
    </form>
  );
}

function getCsrfToken(): string {
  const meta = document.querySelector<HTMLMetaElement>('meta[name="csrf-token"]');
  if (!meta || !meta.content) {
    throw new Error('CSRF token tidak ditemukan di meta tag.');
  }
  return meta.content;
}

const container = document.getElementById('order-app-mount');

if (container) {
  const csrfToken = getCsrfToken();
  
  // Lakukan hidrasi deterministik
  hydrateRoot(container, <OrderForm csrfToken={csrfToken} />);
}

Pengujian dan Verifikasi Zero-Mismatch

Untuk memastikan tidak ada lagi hidrasi mismatch di lingkungan staging atau produksi:

  • Pantau Node Environment: Jalankan React dalam mode pengembangan (NODE_ENV=development). React hanya menampilkan rincian diff mismatch secara eksplisit pada mode dev. Mode produksi hanya memunculkan kode error numerik (misalnya #418).
  • Validasi Whitespace & Atribut Dinamis: Pastikan Django tidak menambahkan karakter baris baru tak disengaja di dalam tag <input> atau mengubah kapitalisasi atribut seperti readonly vs readOnly.
  • Pemeriksaan Masking Token: Django memutar nilai token per-request jika fitur CSRF token masking aktif. Jangan membaca token dari request lama atau dari cache lokal (localStorage); selalu baca dari payload dokumen saat ini (DOM/Cookie).