Hydration mismatch merupakan anomali umum saat mengimplementasikan Server-Side Rendering (SSR) pada framework modern, termasuk pada ekosistem fullstack Rust. Masalah ini terjadi ketika tree DOM statis yang di-render oleh backend Actix Web tidak cocok dengan Virtual DOM (VDOM) awal yang dihitung oleh Dioxus pada klien WebAssembly (WASM). Akibatnya, runtime klien terpaksa membuang nodus yang ada atau memicu full re-render, yang merusak performa serta menyebabkan flickering.

Akar Penyebab Hydration Mismatch pada Rust Fullstack

Desinkronisasi DOM antara Actix Web dan klien Dioxus umumnya bersumber dari tiga faktor utama:

  • Initial State Tidak Sinkron: Server me-render komponen menggunakan data aktual (misalnya dari database), sementara klien WASM melakukan inisialisasi menggunakan Default::default() atau state kosong karena data tidak dioper secara eksplisit.
  • Evaluasi Non-Deterministik: Penggunaan fungsi seperti chrono::Utc::now(), generator UUID, atau dependensi lingkungan server (seperti HTTP header lokal) langsung di dalam badan render komponen. Perbedaan waktu eksekusi milidetik antara server dan browser akan menghasilkan output teks atau atribut yang berbeda.
  • Ketidaklengkapan Serialisasi Serde: Penggunaan atribut #[serde(skip)] atau field opsional yang menghasilkan representasi berbeda saat struct diubah ke JSON dan dibaca kembali oleh WASM.

Metode Debugging di Browser Console

Saat terjadi mismatch, browser console sering kali menampilkan warning bahwa atribut atau text node tidak sesuai. Langkah isolasi masalah meliputi:

  1. Aktifkan console_error_panic_hook dan logger pada klien WASM (misalnya wasm_logger::init(wasm_logger::Config::default());) untuk menangkap tracing panic saat proses hidrasi.
  2. Bandingkan payload raw HTML yang dikirim Actix Web (melalui tab Network > Response) dengan DOM tree yang dihasilkan di tab Elements.
  3. Cari perbedaan pada nodus spesifik: atribut seperti class, conditional elements (misal tag <span> yang hanya muncul saat logged in), atau whitespace pada text node.

Serialisasi dan Injeksi Payload di Actix Web

Solusi arsitektural untuk mencegah mismatch adalah merender komponen di backend dengan initial state tertentu, lalu menyematkan state yang sama persis dalam format JSON ke dalam dokumen HTML menggunakan tag <script type="application/json">.

Definisikan shared struct yang mengimplementasikan Serialize dan Deserialize:

use serde::{Deserialize, Serialize};

#[derive(Serialize, Deserialize, Clone, PartialEq)]
pub struct AppProps {
    pub post_id: u32,
    pub title: String,
    pub rendered_at: String, // String ISO deterministik, bukan raw timestamp dinamis
}

Implementasikan handler pada Actix Web untuk me-render Dioxus VDOM dan menyematkan state JSON:

use actix_web::{get, web, HttpResponse, Responder};
use dioxus::prelude::*;
use crate::shared::{App, AppProps};

#[get("/post/{id}")]
pub async fn render_post(path: web::Path<u32>) -> impl Responder {
    let post_id = path.into_inner();
    
    // Siapkan data deterministik
    let props = AppProps {
        post_id,
        title: "Tutorial Hydration Rust".to_string(),
        rendered_at: "2024-01-01T00:00:00Z".to_string(),
    };

    // Render komponen Dioxus ke HTML string
    let mut vdom = VirtualDom::new_with_props(App, props.clone());
    vdom.rebuild_in_place();
    let body_html = dioxus_ssr::render(&vdom);

    // Serialisasi props untuk payload hidrasi klien
    let serialized_state = serde_json::to_string(&props).unwrap_or_default();

    let full_html = format!(
        r#"<!DOCTYPE html>
<html>
  <head><title>SSR App</title></head>
  <body>
    <div id="main">{}</div>
    <script id="app-state" type="application/json">{}</script>
    <script type="module" src="/pkg/client.js"></script>
  </body>
</html>"#,
        body_html, serialized_state
    );

    HttpResponse::Ok().content_type("text/html; charset=utf-8").body(full_html)
}

Menerima State pada Entry Point Klien Dioxus

Pada target WASM, aplikasi membaca payload JSON dari DOM sebelum Virtual DOM diinisialisasi. Dengan memasukkan struct props yang sama ke konfigurasi peluncuran Dioxus, hidrasi berlangsung deterministik tanpa modifikasi nodus DOM.

use dioxus::prelude::*;
use web_sys::window;
use crate::shared::{App, AppProps};

fn main() {
    console_error_panic_hook::set_once();

    // Ekstraksi initial state dari tag script HTML
    let initial_props: AppProps = window()
        .and_then(|w| w.document())
        .and_then(|doc| doc.get_element_by_id("app-state"))
        .and_then(|el| el.text_content())
        .and_then(|json| serde_json::from_str(&json).ok())
        .expect("Gagal membaca serialized state untuk hidrasi");

    // Launch aplikasi dengan mode hydration aktif
    dioxus_web::launch_with_props(
        App,
        initial_props,
        dioxus_web::Config::new().hydrate(true)
    );
}
Catatan: Hindari pemanggilan logika non-deterministik di level root komponen. Operasikan logika dinamis sisi klien (seperti use_future atau use_effect) hanya setelah hidrasi awal selesai guna mempertahankan keselarasan VDOM dengan initial state dari server.