Hydration mismatch terjadi ketika representasi Virtual DOM (VDOM) yang dihasilkan oleh Reagent/React di browser tidak identik dengan HTML string hasil render Server-Side Rendering (SSR) dari runtime JVM. Pada aplikasi data analitik berorientasi kolom (columnar dataset), masalah ini sering memicu error konsol React (#418 atau #423), hilangnya interaktivitas event listener, hingga re-render penuh yang merusak performa initial page load.
Akar Masalah Hydration Mismatch pada Tabel Kolom
Penyebab utama diskrepansi DOM antara SSR Clojure JVM dan Reagent client-side umumnya berakar pada dua hal berikut:
1. Perbedaan Representasi Float dan Nil/Null
Pada Clojure JVM, tipe data numerik presisi tinggi sering kali berupa java.lang.Double, BigDecimal, atau rasio. Saat dikonversi ke HTML via library templating JVM (seperti Hiccup), nilai float dicetak menggunakan str bawaan Java (misal: 1.0). Sebaliknya, JavaScript runtime memformat angka 1.0 menjadi string "1" saat diubah ke teks DOM.
Selain itu, penanganan nil pada vector kolom sering menghasilkan perbedaan atribut: JVM dapat merender atribut kosong sebagai string kosong class="", sementara client Reagent menghilangkannya sama sekali dari DOM.
2. Urutan Evaluasi Lazy Sequence dan Chunked Vector
Dataset analitik kolom berskala besar sering diproses menggunakan lazy sequence atau chunked sequence (kelompok 32 elemen) pada JVM untuk efisiensi memori. Jika pipeline rendering SSR memotong sequence menggunakan iterator lazy yang terpengaruh mutasi state eksternal, sedangkan client mengevaluasinya secara eager via vec, maka jumlah baris (<tr>) yang di-hydrate tidak akan selaras.
Langkah 1: Normalisasi Skema Data Sebelum Render
Hindari merender data mentah langsung ke komponen Hiccup. Terapkan fungsi sanitasi deterministik di kedua platform (Cljc) guna memastikan keseragaman string representasi numerik dan nilai kosong.
(ns my-app.analytics.normalizer)
(defn format-metric [v]
(cond
(nil? v) "-"
(number? v) (.toFixed v 2)
:else (str v)))
(defn normalize-columns [columns]
;; columns: {:timestamps [...] :metrics [...]}
(reduce-kv
(fn [acc col-key values]
(assoc acc col-key (mapv format-metric values)))
{}
columns))Penggunaan mapv memastikan vektor dievaluasi secara eager dan seragam, menghilangkan efek samping lazy chunking.
Langkah 2: Pemisahan State Melalui Transfer State JSON
Jangan mengandalkan transfer struktur VDOM kompleks dari server ke client secara implisit. Gunakan script tag transfer state murni (JSON) pada template SSR HTML.
;; SSR (JVM Clojure)
(defn render-page [initial-dataset]
[:html
[:body
[:div#app (reagent.dom.server/render-to-static-markup [analytics-table initial-dataset])]
[:script#preloaded-data
{:type "application/json"
:dangerouslySetInnerHTML {:__html (cheshire.core/generate-string initial-dataset)}}]
[:script {:src "/js/main.js"}]]])Pada sisi browser (ClojureScript), baca JSON mentah tersebut untuk menginisialisasi Reagent Atom sebelum menjalankan mount hydration:
;; Client (ClojureScript)
(ns my-app.client
(:require [reagent.dom.client :as rdc]
[my-app.analytics.views :refer [analytics-table]]))
(defn get-preloaded-data []
(when-let [el (.getElementById js/document "preloaded-data")]
(js->clj (.parse js/JSON (.-textContent el)) :keywordize-keys true)))
(defn ^:export init []
(let [root (rdc/create-root (.getElementById js/document "app"))
initial-state (get-preloaded-data)]
;; Gunakan hydrate-root bila menggunakan React 18+
(rdc/hydrate-root root [analytics-table initial-state])))Langkah 3: Isolasi Agregasi Kolom via Dynamic Client Island
Komputasi kolom analitik yang dinamis (seperti pengurutan dinamis, pivot table, atau agregasi percentile) rentan menghasilkan drift DOM jika dijalankan selama siklus render hydration. Pisahkan rendering tabel statis dasar dari logika interaktif analitik.
(ns my-app.analytics.views
(:require [reagent.core :as r]))
(defn- client-sorting-bar [state]
(let [is-mounted (r/atom false)]
(r/create-class
{:component-did-mount #(reset! is-mounted true)
:reagent-render
(fn []
(if-not @is-mounted
;; Skeleton statis yang sama dengan SSR
[:div.toolbar-skeleton "Memuat kontrol..."]
;; Kontrol interaktif diaktifkan pasca-hydration
[:div.toolbar
[:button {:on-click #(swap! state update :sort-dir not)} "Ubah Urutan"]]))})))
(defn analytics-table [columns]
[:div.analytics-container
[client-sorting-bar]
[:table
[:tbody
;; Render baris berbasis data kolom normal
(for [idx (range (count (:timestamps columns)))]
^{:key idx}
[:tr
[:td (get-in columns [:timestamps idx])]
[:td (get-in columns [:metrics idx])]])]]])Verifikasi dan Pemecahan Masalah
Jika hydration mismatch masih muncul:
- Jalankan inspect pada element yang memicu warning via konsol browser. Periksa apakah letak perbedaan berada pada spasi teks (whitespace handling) atau perbedaan atribut boolean.
- Gunakan
goog.DEBUGuntuk mengaktifkan log Reagent yang menampilkan perbedaan spesifik tree hiccup. - Bila tabel memiliki lebih dari 10.000 titik data kolom, jangan hydrate baris tabel di thread utama. Gunakan SSR hanya untuk 50 baris pertama (preview) dan serahkan rendering sisa baris kepada Web Worker atau virtualized list (cljs-react-window) setelah event
component-did-mountselesai dieksekusi.
Komentar
0 komentar
Masuk ke akun kamu untuk ikut berkomentar.
Belum ada komentar
Jadilah yang pertama ikut berdiskusi!