Root Cause: Mengapa Hydration Panic Terjadi di Leptos SSR

Hydration pada Leptos SSR bekerja dengan cara memasang event listener dan mengaitkan reaktivitas sinyal client-side ke struktur DOM statis hasil render server. Proses ini tidak membuat ulang elemen HTML dari nol. Leptos menelusuri pohon DOM (DOM walking) yang sudah ada di browser berdasarkan penanda hidrasi (hydration markers) yang ditanamkan oleh server.

Ketika client WASM mengharapkan node teks atau elemen tertentu tetapi mendapati struktur yang berbeda di live DOM, Leptos memicu panic! di dalam runtime WASM. Panic ini umumnya bermanifestasi sebagai pemanggilan unwrap() atau expect() yang gagal saat meng-cast web_sys::Node atau saat iterator rekonsiliasi kehabisan node.

Model Keamanan Memori Rust vs WASM Runtime

Dalam C/C++, inkonsistensi layout memori atau desinkronisasi state biasanya berujung pada dangling pointer, use-after-free, atau segmentation fault. Di WebAssembly, Rust berjalan di dalam ruang memori linier terisolasi (sandboxed linear memory). Rust menjamin ketiadaan data race dan memory corruption di dalam batas linear memory tersebut.

Namun, interaksi antara WASM dan browser DOM melintasi batas FFI (Foreign Function Interface) melalui wasm-bindgen. Masalah pada hidrasi Leptos bukan merupakan buffer overflow, melainkan:

  • Logical Desynchronization: WASM runtime berasumsi bahwa representasi DOM host persis dengan output kompilasi virtual render server.
  • Unsafe Invariants Violation: Melalui penelusuran DOM host, ekspektasi tipe elemen yang salah memaksa Leptos memanggil unwrap pada Option kosong.
  • Panic Unwinding Boundary: Ketika Rust panic terjadi di dalam WASM tanpa registrasi panic hook yang tepat, unwinding melewati batas FFI JS-WASM akan menyebabkan WASM trap, menghentikan eksekusi thread WASM secara permanen dan mematikan seluruh interaktivitas client.

Anatomi Masalah: State Mismatch antara Server dan Client

Penyebab paling sering dari hydration panic adalah evaluasi nilai non-deterministik atau kegagalan sinkronisasi state data antara server dan client. Berikut adalah contoh anti-pattern yang menghasilkan panic saat hidrasi:

// ANTI-PATTERN: Menghasilkan DOM berbeda antara SSR dan Client Hydration
use leptos::*;

#[component]
pub fn BrokenClock() -> impl IntoView {
    // BUG: Evaluasi waktu lokal menghasilkan string berbeda
    // antara server (node SSR) dan client (WASM runtime).
    let current_time = move || {
        #[cfg(feature = "ssr")]
        {
            "2026-03-30 12:00:00 UTC".to_string()
        }
        #[cfg(not(feature = "ssr"))]
        {
            js_sys::Date::new_0().to_locale_string("id-ID", &JsValue::NULL).as_string().unwrap()
        }
    };

    view! {
        <div class="clock-container">
            <p>{current_time()}</p>
        </div>
    }
}

Pada kode di atas, server menghasilkan DOM teks dengan nilai waktu statis. Ketika runtime WASM diinisialisasi pada browser, fungsi current_time() menghasilkan format tanggal lokal browser. DOM walker mendeteksi ketidakcocokan antara text node hasil render server dengan text node yang dihidrasi oleh WASM, memicu kepanikan rekonsiliasi:

panicked at 'hydration mismatch: expected text node containing "...", found "..."'

Implementasi State Transfer Deterministik

Untuk menghindari mismatch, state yang dihitung atau diambil di server harus diserialisasikan ke dalam HTML respons dan dideserialisasikan secara identik oleh client WASM. Leptos menyediakan abstraksi Resource yang secara otomatis menangani serialisasi state ini jika fitur SSR diaktifkan dengan benar.

use leptos::*;
use serde::{Deserialize, Serialize};

#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
pub struct UserSession {
    pub username: String,
    pub permissions: Vec<String>,
}

// Server function untuk mengambil data secara aman
#[server(GetSessionData, "/api")]
pub async fn get_session_data() -> Result<UserSession, ServerFnError> {
    // Simulasi pengambilan session dari auth layer server
    Ok(UserSession {
        username: "developer".to_string(),
        permissions: vec!["read".to_string(), "write".to_string()],
    })
}

#[component]
pub fn ProfileView() -> impl IntoView {
    // Resource menduplikasi state server ke client via JSON payload tersembunyi
    let session_resource = create_resource(|| (), |_| async move { get_session_data().await });

    view! {
        <div class="profile">
            <Suspense fallback=move || view! { <p>"Memuat data sesi..."</p> }>
                {move || {
                    session_resource.get().map(|result| match result {
                        Ok(data) => view! {
                            <div class="session-data">
                                <span>"User: " {data.username}</span>
                            </div>
                        }.into_view(),
                        Err(_) => view! { <p>"Gagal memuat sesi."</p> }.into_view(),
                    })
                }}
            </Suspense>
        </div>
    }
}

Mekanisme ini bekerja karena Leptos menyuntikkan script tag yang berisi state serialisasi Serde JSON di akhir dokumen HTML. Saat client WASM melakukan takeover, Resource tidak memanggil endpoint server kembali, melainkan membaca buffer data serialisasi yang sudah ada di DOM untuk melakukan rehidrasi deterministik.

Checklist Verifikasi Konsistensi DOM dan State

Gunakan langkah-langkah berikut untuk mengaudit dan mendebug hydration mismatch sebelum merilis aplikasi ke staging atau production:

  1. Aktifkan Panic Hook yang Informatif: Pastikan inisialisasi client memanggil console_error_panic_hook::set_once() di dalam fungsi main client WASM agar stack trace tercetak jelas di console browser.
  2. Periksa Conditional Compilation: Hindari percabangan #[cfg(feature = "ssr")] di dalam tubuh rendering komponen yang menentukan struktur pohon elemen (misalnya merender tag <button> hanya di client). Gunakan komponen <ClientOnly> untuk komponen non-SSR.
  3. Audit Operasi Non-Deterministik: Jangan gunakan operasi pseudo-random number generator (PRNG), penanda waktu sistem (Instant, SystemTime), atau locale string langsung di dalam blok view! tanpa membungkusnya dalam sinyal yang diupdate pasca-hidrasi (misalnya di dalam create_effect).
  4. Validasi Nesting HTML: Browser sering kali secara otomatis memperbaiki nesting HTML yang tidak valid (contoh: meletakkan <div> di dalam <p>, atau tidak menyertakan <tbody> di dalam <table>). Koreksi otomatis oleh parser browser ini mengubah struktur pohon DOM sebelum WASM mulai melakukan hidrasi, yang berujung pada kegagalan traversal node oleh Leptos.
  5. Validasi Skema Serde: Pastikan struct data yang di-share antara server dan WASM client memiliki definisi field dan atribut serialisasi (#[serde(...)]) yang 100% kompatibel guna menghindari desinkronisasi payload state.