Pada aplikasi Rust SSR dengan Yew, hydration mismatch terjadi ketika HTML yang dirender di server tidak lagi mewakili state awal yang dihitung ulang di sisi klien. Akibatnya, UI bisa berubah sesaat setelah halaman dimuat, event binding dapat bermasalah, dan proses hydration menjadi tidak stabil.

Masalah ini hampir selalu berakar pada satu hal: render pertama di server dan render pertama di klien tidak deterministik. Jika komponen membaca waktu saat ini, angka acak, local storage, ukuran viewport, atau data browser-only lain saat membentuk state awal, maka markup awal bisa berbeda. Solusi utamanya adalah memastikan initial state yang dipakai server dan klien benar-benar sama, lalu menunda pembacaan nilai yang hanya tersedia di browser sampai setelah hydration selesai.

Mengapa hydration mismatch terjadi pada Yew SSR

Pada mode SSR, alur sederhananya seperti ini:

  1. Server merender komponen Yew menjadi HTML.
  2. Browser menerima HTML dan menampilkannya.
  3. Yew di klien melakukan hydration terhadap DOM yang sudah ada.
  4. Jika struktur atau isi yang diharapkan klien berbeda dari HTML server, muncul mismatch.

Karena itu, hydration bukan sekadar render biasa. Ia mengasumsikan bahwa DOM yang sudah ada adalah hasil dari state awal yang identik. Jika asumsi ini salah, framework harus menyesuaikan ulang DOM atau gagal menjaga konsistensi UI.

Gejala umum

  • Teks berubah segera setelah JavaScript/WASM selesai dimuat.
  • Konten “berkedip” dari versi server ke versi klien.
  • Urutan item daftar berubah tanpa interaksi pengguna.
  • Class CSS atau atribut tertentu tiba-tiba berganti setelah hydration.
  • State input, tema, atau layout tidak sama dengan yang tampil dari server.

Jika halaman SSR terlihat benar saat HTML awal tampil, tetapi berubah beberapa milidetik kemudian tanpa aksi pengguna, itu indikator kuat adanya hydration mismatch atau state awal yang tidak stabil.

Root cause: sumber state awal yang tidak deterministik

1. Waktu saat ini

Contoh paling umum adalah memanggil waktu saat render:

#[function_component(TimeBadge)]
fn time_badge() -> Html {
    let now = js_sys::Date::new_0().to_locale_string("id-ID");

    html! {
        <span>{ format!("Dimuat pada: {}", now) }</span>
    }
}

Kode seperti ini bermasalah karena server dan klien hampir pasti menghasilkan waktu berbeda. Bahkan selisih kecil sudah cukup untuk memicu mismatch teks.

2. Nilai acak

ID acak, warna acak, urutan acak, atau token acak yang dibuat saat render akan menghasilkan output yang tidak sama antara server dan klien kecuali Anda menyuntikkan nilainya dari sumber yang sama.

3. Local storage, session storage, cookie browser-only

Server tidak memiliki akses ke Web Storage. Jika state awal dibangun dari local storage di klien, maka nilai pada render pertama klien bisa berbeda dari HTML yang sudah dirender server.

4. Ukuran viewport atau media query yang dihitung di render awal

Lebar layar, tinggi viewport, atau hasil pembacaan window tidak tersedia di server. Bila komponen langsung merender layout berbeda berdasarkan ukuran browser saat hydration dimulai, hasilnya bisa tidak cocok.

5. Data asinkron yang tidak diselaraskan

Jika server sudah merender berdasarkan data A tetapi klien memulai dengan state kosong lalu segera memuat data B, tampilan awal bisa berubah. Ini bukan selalu bug SSR, tetapi jika perubahan terjadi sebelum hydration stabil, hasilnya sering terlihat seperti mismatch.

Pola yang tidak aman vs pola state awal yang aman

Contoh sebelum: state awal dihitung dari browser saat render

Berikut pola yang sering memicu masalah:

use yew::prelude::*;

#[function_component(ThemeLabel)]
fn theme_label() -> Html {
    let theme = use_state(|| {
        // Browser-only dan tidak tersedia di server
        let window = web_sys::window().unwrap();
        let storage = window.local_storage().unwrap().unwrap();
        storage.get_item("theme").unwrap().unwrap_or_else(|| "light".into())
    });

    html! {
        <p>{ format!("Tema aktif: {}", *theme) }</p>
    }
}

Masalahnya jelas: server tidak bisa membaca local storage, sedangkan klien bisa. Maka state awal di kedua sisi berpotensi berbeda.

Contoh sesudah: gunakan state awal stabil, sinkronkan nilai browser-only setelah mount

Pola yang lebih aman adalah memakai nilai awal yang sama di server dan klien, lalu mengubahnya di efek setelah hydration:

use yew::prelude::*;

#[function_component(ThemeLabel)]
fn theme_label() -> Html {
    let theme = use_state(|| String::from("light"));

    {
        let theme = theme.clone();
        use_effect_with((), move |_| {
            if let Some(window) = web_sys::window() {
                if let Ok(Some(storage)) = window.local_storage() {
                    if let Ok(Some(saved)) = storage.get_item("theme") {
                        if saved != *theme {
                            theme.set(saved);
                        }
                    }
                }
            }

            || ()
        });
    }

    html! {
        <p>{ format!("Tema aktif: {}", *theme) }</p>
    }
}

Pendekatan ini bekerja karena HTML server dan render awal klien sama-sama memakai "light". Jika browser memiliki preferensi berbeda, perubahan dilakukan setelah hydration, bukan saat membangun DOM awal.

Konsekuensi pola aman

Trade-off dari pola ini adalah kemungkinan ada perubahan UI setelah mount, misalnya tema berganti dari light ke dark. Namun perubahan itu bersifat terkontrol dan jauh lebih aman daripada membuat render awal tidak konsisten.

Strategi utama: serialisasi state dari server ke klien

Jika server memang sudah mengetahui data awal yang diperlukan untuk render, strategi paling stabil adalah merender HTML dan mengirim state yang sama ke klien. Dengan begitu, hydration berjalan di atas input identik.

Kapan strategi ini cocok

  • Data halaman berasal dari backend dan sudah tersedia saat SSR.
  • Anda ingin menghindari fetch ulang yang mengubah UI saat hydration.
  • Komponen memerlukan state awal non-trivial, misalnya user profile, daftar item, atau konfigurasi halaman.

Pola umum

  1. Server menyiapkan sebuah struct state awal.
  2. State dirender menjadi HTML untuk SSR.
  3. State yang sama disisipkan ke halaman, biasanya sebagai JSON.
  4. Klien membaca JSON itu dan menggunakannya sebagai input awal aplikasi.

Contoh bentuk state bersama

use serde::{Deserialize, Serialize};

#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
struct AppState {
    username: String,
    notifications: u32,
    theme: String,
}

Contoh ide penyisipan state ke HTML

Di sisi server, Anda bisa menyisipkan JSON state ke dalam tag script non-eksekusi:

<script id="initial-state" type="application/json">
{"username":"dina","notifications":3,"theme":"light"}
</script>

Lalu di klien, baca isi elemen tersebut sebelum mount aplikasi, parse menjadi AppState, dan gunakan sebagai input awal. Nama fungsi dan detail integrasi bergantung pada arsitektur aplikasi Anda, tetapi prinsipnya adalah: jangan hitung ulang state awal dari sumber berbeda jika server sudah memilikinya.

Mengapa ini efektif

Hydration mismatch muncul ketika ada dua sumber kebenaran berbeda antara server dan klien. Dengan serialisasi state, Anda memaksa keduanya memakai satu snapshot yang sama pada render pertama.

Catatan keamanan dan implementasi

  • Pastikan JSON yang disisipkan di-escape dengan benar agar tidak merusak HTML.
  • Jangan masukkan data sensitif yang tidak memang harus dikirim ke browser.
  • Jaga agar struktur state tetap minimal; kirim hanya yang diperlukan untuk render awal.

Pemisahan logika browser-only dari render awal

Banyak mismatch muncul karena logika browser-only bercampur langsung dengan proses render. Solusi praktisnya adalah memisahkan tiga kategori data:

  • Data universal: tersedia di server dan klien, aman dipakai saat render awal.
  • Data server-derived: diketahui server, lalu diserialisasi ke klien.
  • Data browser-only: hanya boleh dibaca setelah mount atau melalui fallback yang stabil.

Contoh pendekatan aman untuk viewport

Jangan lakukan ini pada render awal:

let is_mobile = web_sys::window()
    .and_then(|w| w.inner_width().ok())
    .and_then(|v| v.as_f64())
    .map(|w| w < 768.0)
    .unwrap_or(false);

Lebih aman jika render awal memakai layout netral atau CSS responsif, lalu hanya gunakan JavaScript/WASM untuk peningkatan perilaku setelah hydration. Jika keputusan layout bisa ditangani CSS, itu biasanya pilihan terbaik karena tidak membuat HTML awal bergantung pada API browser.

Contoh pendekatan aman untuk waktu

Jika Anda perlu menampilkan timestamp “saat halaman dibuat”, hitung di server dan kirim nilainya ke klien. Jika Anda perlu jam lokal pengguna, tampilkan placeholder stabil terlebih dahulu, lalu perbarui setelah mount.

Sebelum dan sesudah: komponen dengan data non-deterministik

Sebelum

#[function_component(Greeting)]
fn greeting() -> Html {
    let name = if js_sys::Math::random() > 0.5 {
        "Pagi"
    } else {
        "Sore"
    };

    html! {
        <h2>{ format!("Selamat {}", name) }</h2>
    }
}

Render ini tidak bisa dijamin sama antara server dan klien.

Sesudah

use yew::prelude::*;

#[derive(Clone, PartialEq)]
struct GreetingProps {
    period: AttrValue,
}

#[function_component(Greeting)]
fn greeting(props: &GreetingProps) -> Html {
    html! {
        <h2>{ format!("Selamat {}", props.period) }</h2>
    }
}

Di sini, keputusan “Pagi” atau “Sore” ditentukan satu kali oleh server atau sumber state bersama, lalu dipakai identik di klien.

Checklist debugging hydration mismatch pada Yew

Jika Anda menduga ada mismatch, gunakan checklist berikut:

  1. Bandingkan HTML server dan render awal klien
    Periksa bagian UI yang berubah segera setelah load. Fokus pada teks, atribut, urutan list, dan conditional rendering.
  2. Cari sumber non-deterministik
    Audit penggunaan waktu, random, local storage, session storage, cookie browser-only, viewport, locale browser, dan API window/document.
  3. Periksa initializer state
    Lihat semua use_state, props builder, dan fungsi yang dipanggil saat render awal. Initializer harus aman dijalankan di server dan klien dengan hasil sama.
  4. Pindahkan pembacaan browser-only ke effect
    Jika nilai hanya tersedia di browser, baca setelah mount dan terima bahwa UI mungkin diperbarui setelah hydration.
  5. Pastikan data SSR dan data klien sinkron
    Jika server merender dari data tertentu, kirim snapshot data itu ke klien agar tidak memulai dari state kosong atau data berbeda.
  6. Hindari random key atau ID saat render
    Untuk daftar, gunakan identifier stabil dari data, bukan hasil acak.
  7. Periksa formatting yang bergantung locale
    Format tanggal/angka bisa berbeda jika server dan browser memakai locale berbeda. Jika perlu, serialisasikan hasil format final atau gunakan format yang konsisten.

Trade-off SSR vs interaktivitas awal

Tidak semua nilai cocok ditentukan saat SSR. Ada kompromi yang perlu dipahami:

Pilih SSR-stable lebih dulu jika:

  • Anda ingin HTML awal konsisten dan minim flicker.
  • Konten awal penting untuk keterbacaan dan SEO.
  • Data utama halaman sudah diketahui server.

Pilih update setelah mount jika:

  • Data hanya tersedia di browser.
  • Nilai benar-benar personal untuk perangkat pengguna, seperti viewport atau local storage.
  • Perubahan kecil setelah mount dapat diterima.

Pendekatan praktis di dunia nyata

Sering kali solusi terbaik bukan memaksa semua hal masuk ke SSR, tetapi membagi prioritas:

  • SSR untuk konten inti dan state yang bisa dibuat deterministik.
  • Hydration untuk event dan interaksi.
  • Enhancement setelah mount untuk preferensi browser-only.

Dengan pola ini, Anda menjaga pengalaman awal tetap stabil tanpa mengorbankan kebutuhan interaktivitas yang memang bergantung pada browser.

Ringkasan pola aman untuk mencegah hydration mismatch

  • Pastikan render pertama di server dan klien memakai state awal yang sama.
  • Jangan baca waktu saat ini, random, local storage, viewport, atau API browser lain saat membangun markup awal.
  • Jika server sudah punya data, serialisasikan state dan gunakan snapshot yang sama di klien.
  • Pindahkan logika browser-only ke effect setelah mount.
  • Gunakan fallback atau placeholder yang stabil saat data browser-only belum tersedia.
  • Prioritaskan CSS untuk responsivitas jika memungkinkan, daripada menentukan layout dari JavaScript saat render awal.

Inti dari Rust SSR: cegah hydration mismatch pada Yew dengan state awal stabil adalah menjaga determinisme render pertama. Selama HTML server dan state awal klien berasal dari sumber yang sama dan aturan yang sama, hydration akan jauh lebih dapat diprediksi, mudah diuji, dan minim kejutan di produksi.