Hydration mismatch terjadi ketika representasi pohon DOM yang dirender oleh server (SSR) tidak identik dengan pohon DOM virtual yang dihitung oleh framework frontend (seperti React, Vue, atau Alpine.js) saat proses hidrasi awal di browser. Pada arsitektur hybrid Django—di mana template Django merender markup awal lalu komponen frontend melakukan mount atau hydration—masalah ini kerap memicu peringatan konsol, hilangnya event listener, hingga perombakan UI secara visual (flicker).
Akar masalah utama berasal dari cara data backend disuntikkan ke konteks browser, manipulasi whitespace oleh Django Template Language (DTL), dan perbedaan lingkungan antara server serta klien. Solusi paling stabil untuk kendala ini adalah isolasi serialisasi data menggunakan tag template bawaan Django: json_script.
Akar Masalah Hydration Mismatch pada Django SSR
Penyebab ketidakcocokan DOM antara server dan klien dalam ekosistem Django umumnya terbagi ke dalam tiga faktor teknis:
1. Perbedaan Serialisasi Data Python vs JavaScript
Praktik umum yang keliru adalah mencetak objek Python langsung ke dalam blok skrip template:
<script>
const INITIAL_STATE = {{ data|safe }};
</script>Pendekatan ini berisiko XSS dan rentan parsing mismatch. Tipe data Python seperti True, False, dan None akan dicetak sebagai kata kunci yang tidak valid atau menghasilkan evaluasi berbeda jika tidak diformat tepat menjadi true, false, dan null.
2. Injeksi Whitespace oleh Django Template Language (DTL)
DTL mempertahankan spasi dan baris baru (newline) di sekitar tag logika seperti {% if %}, {% for %}, dan {% endif %}. Jika framework frontend mengharapkan empty text node atau struktur elemen tanpa spasi di antara tag, perbedaan spasi sekecil satu karakter baris baru akan memicu kegagalan hidrasi.
3. Desinkronisasi Timezone dan Locale
Server Django mengeksekusi render berbasis zona waktu server (atau konfigurasi TIME_ZONE pada settings.py). Jika klien memformat tanggal langsung pada tahap render pertama menggunakan objek Date lokal browser (misalnya WIB vs UTC), string teks yang dihasilkan server dan klien akan berbeda.
Serialisasi State Aman Menggunakan json_script
Tag bawaan json_script mengatasi masalah serialisasi dan keamanan XSS. Tag ini mengonversi dictionary atau list Python ke dalam string JSON yang valid, lalu menyematkannya ke dalam elemen HTML <script type="application/json" id="..."> dengan pengamanan karakter escape (seperti <, >, dan &).
1. Konfigurasi View: Standardisasi ISO-8601
Hindari mengirim objek tanggal mentah atau format lokal server ke template. Format seluruh tanggal ke ISO-8601 di lapisan view Django.
# views.py
from django.shortcuts import render
from django.utils import timezone
def product_detail_view(request, product_id):
# ponytail: optimasi query ORM lewat values() jika payload membesar
payload = {
"id": product_id,
"title": "Mechanical Keyboard Wireless",
"price": 1250000,
"is_in_stock": True,
"published_at": timezone.now().isoformat(),
"features": ["Hot-swappable", "Bluetooth 5.0", "RGB Backlit"],
}
return render(request, "products/detail.html", {"initial_state": payload})2. Template DTL: Injeksi State Terisolasi
Sematkan state menggunakan json_script tanpa tag |safe tambahan. Pastikan markup tempat hidrasi bersih dari whitespace tak terkontrol.
{# templates/products/detail.html #}
{% load static %}
<!DOCTYPE html>
<html lang="id">
<head>
<meta charset="UTF-8">
<title>Detail Produk</title>
</head>
<body>
{# Serialisasi state aman ke elemen JSON terisolasi #}
{{ initial_state|json_script:"product-state" }}
{# Target container untuk hidrasi #}
<div id="product-root" data-server-rendered="true">{# Hindari spasi acak jika server-side rendered HTML disisipkan di sini #}</div>
<script type="module" src="{% static 'js/bundle.js' %}"></script>
</body>
</html>Implementasi Hydration pada Frontend: React dan Alpine.js
Frontend membaca JSON dari elemen DOM melalui ID yang didefinisikan pada json_script, lalu menggunakan data tersebut sebagai state awal yang identik.
Integrasi dengan React
Saat menggunakan React 18+, gunakan hydrateRoot jika konten awal juga dirender di server, atau createRoot jika melakukan client mount pada target kosong.
// src/index.jsx
import React, { useState, useEffect } from 'react';
import { hydrateRoot } from 'react-dom/client';
function ProductApp({ initialState }) {
const [displayDate, setDisplayDate] = useState(initialState.published_at);
useEffect(() => {
// Defer formatting lokal browser hingga komponen ter-mount untuk hindari mismatch
setDisplayDate(new Date(initialState.published_at).toLocaleDateString());
}, [initialState.published_at]);
return (
<div className="product-card">
<h1>{initialState.title}</h1>
<p>Harga: Rp {initialState.price.toLocaleString('id-ID')}</p>
<p>Status: {initialState.is_in_stock ? 'Tersedia' : 'Habis'}</p>
<small>Dipublikasikan: {displayDate}</small>
</div>
);
}
const stateNode = document.getElementById('product-state');
const rootContainer = document.getElementById('product-root');
if (stateNode && rootContainer) {
const initialState = JSON.parse(stateNode.textContent);
hydrateRoot(rootContainer, <ProductApp initialState={initialState} />);
}Integrasi Ringan dengan Alpine.js
Alpine.js sering digunakan bersama DTL untuk interaktivitas ringan. State dari json_script dapat dimuat langsung ke dalam store atau komponen x-data.
{# templates/products/alpine_detail.html #}
{{ initial_state|json_script:"alpine-product-state" }}
<div x-data="productComponent()" class="product-container">
<h1 x-text="state.title"></h1>
<button :disabled="!state.is_in_stock">Beli Sekarang</button>
</div>
<script>
document.addEventListener('alpine:init', () => {
Alpine.data('productComponent', () => ({
state: JSON.parse(document.getElementById('alpine-product-state').textContent),
}));
});
</script>Aturan Menjaga Konsistensi DOM SSR dan Client
- Tunda Evaluasi Spesifik Lingkungan Klien: Properti seperti
window.innerWidth, status autentikasi berbasislocalStorage, dan konversi zona waktu lokal klien tidak boleh menentukan struktur render pertama. Lakukan mutasi nilai tersebut di dalam hook siklus hidup klien (useEffectdi React atauinit()di Alpine.js). - Gunakan Tag
{% spaceless %}Jika Diperlukan: Apabila markup server dihasilkan oleh DTL dan disuntikkan ke dalam virtual DOM React SSR, bungkus blok tersebut dengan tag{% spaceless %}untuk menghapus whitespace di antara tag HTML. - Validasi Keberadaan Node Sebelum Parse: Hindari runtime error dengan memeriksa keberadaan elemen script sebelum menjalankan
JSON.parse().
Catatan Keamanan: Jangan gunakan filter
|safelangsung pada string variabel JSON di dalam tag skrip inline. Tagjson_scriptsecara eksplisit mengganti karakter berbahaya seperti<menjadi entitas unicode\u003Cguna menutup celah injeksi skrip.
Komentar
0 komentar
Masuk ke akun kamu untuk ikut berkomentar.
Belum ada komentar
Jadilah yang pertama ikut berdiskusi!