Skip to main content

Crate leptos_hydrated

Crate leptos_hydrated 

Source
Expand description

§Leptos Hydrated

A library for flicker-free interactive state hydration in Leptos 0.8.

leptos_hydrated is ideal for bootstrapping state that you already have or can have on both sides (isomorphic data), such as cookies, URL parameters, or locally cached state. By initializing signals immediately with server-provided state and synchronizing them once the browser is active, you eliminate the “loading flicker” common in SSR applications.

§The Problem

In SSR applications there is a gap between the server rendering HTML and the client WASM initialising. If you rely on async resources to bootstrap state the UI flickers from a default/loading state to the real state once JS takes over.

§The Solution

leptos_hydrated synchronises state from the server to the client by:

  1. Server-Side Render (SSR): initial() is called on the server. The result is serialized into the HTML shell.
  2. Hydration: The client reads the serialized state from the HTML and initializes the signal immediately — zero flicker.
  3. Synchronization: Once the WASM is active, initial() is re-run on the client to synchronize with the current browser state (e.g., reading a JS-accessible cookie).
  4. Lifecycle Hooks: Use on_hydrate to set up browser-only event listeners (e.g., network status, window resize).
  5. WASM Bundle Optimization: Use #[hydrated_server] to eliminate serde_json from your client-side WASM bundle.

§Hydration Scopes

leptos_hydrated offers three levels of state scope, ordered by increasing granularity:

§1. Local

Each call to hydrated_signal creates a new, independent hydrated signal. This is the primary entry point for hydrated state. Synchronization is handled automatically via a deterministic hydration counter.

#[component]
fn MyComponent() -> impl IntoView {
    // Always creates a new, independent signal
    let state = hydrated_signal(MyState::initial());
    // ...
}

§2. Scoped

Wrap a section of your component tree with <HydratedContext<T>> to share a hydrated signal. Use use_hydrated_context<T>() in descendants to access it.

#[component]
fn Feature() -> impl IntoView {
    view! {
        <HydratedContext<MyState>>
            <Descendant />
        </HydratedContext<MyState>>
    }
}

#[component]
fn Descendant() -> impl IntoView {
    // Access the shared signal from context (returns Option<RwSignal<T>>)
    let state = use_hydrated_context::<MyState>();
    // ...
}

§3. Global

Use <HydratedContext<T> global=true /> (typically in your app shell). This provides the state globally across your entire application.

#[component]
fn App() -> impl IntoView {
    view! {
        <HydratedContext<MyState> global=true />
        // ...
    }
}

§Bundle Size Optimization

One of the biggest contributors to Leptos WASM bundle size is serde_json. leptos_hydrated provides a custom codec and macro to eliminate this weight from your client-side binary.

§#[hydrated_server]

Replace standard #[server] with #[hydrated_server] to opt into the BrowserJson protocol. This uses the browser’s native JSON.parse and JSON.stringify on the client, and serde_json on the server.

#[hydrated_server]
pub async fn my_server_fn(data: MyData) -> Result<MyData, ServerFnError> {
    Ok(data)
}

By using this macro, you can often save 150 KB - 200 KB on your final WASM binary. It is fully compatible with ActionForm because it uses standard URL encoding for inputs and optimized JSON for outputs.

§Quick Start

Implement the Hydratable trait to define how your state is initialized and synchronized.

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

#[derive(Clone, Default, Serialize, Deserialize, PartialEq, Debug)]
pub struct ThemeState {
    pub theme: String,
}

impl Hydratable for ThemeState {
    fn initial() -> Self {
        // Use isomorphic helpers to read from cookies on both sides.
        let theme = get_cookie("theme").unwrap_or_else(|| "dark".into());
        ThemeState { theme }
    }

    #[cfg(not(feature = "ssr"))]
    fn on_hydrate(&self) {
        // Optional: Execute code in the browser after hydration
        leptos::logging::log!("Hydrated theme: {}", self.theme);
    }
}

#[component]
pub fn App() -> impl IntoView {
    view! {
        // Provide state globally
        <HydratedContext<ThemeState> global=true />

        <MainContent />
    }
}

#[component]
fn MainContent() -> impl IntoView {
    // Consume it anywhere in the tree
    let state = hydrated_signal(ThemeState::initial());
    view! {
        <p>"Theme: " {move || state.get().theme}</p>
    }
}

§Server-Side Setup

You must add the .hydrated() middleware to your Axum router to enable state injection.

let app = Router::new()
    .leptos_routes(...)
    .fallback(...)
    .hydrated() // <--- Add this before .with_state()
    .with_state(leptos_options);

§PWA & “Born Offline” Support

leptos_hydrated supports PWAs loading from an offline shell (CSR mode) by detecting the mounting mode in your lib.rs and providing it via context to your components.

Re-exports§

pub use components::HydratedContext;

Modules§

codec
components

Macros§

isomorphic

Traits§

Hydratable
A trait for types that can be hydrated automatically.

Functions§

get_cookie
Returns the value of a cookie by name.
get_query_param
Returns the value of a query parameter by name.
hydrated_signal
The core hook for creating a hydrated signal.
set_cookie
Sets a cookie on both server and client.
use_hydrated_context
Accesses a hydrated signal of type T from the current context.

Attribute Macros§

hydrate
A macro to gate a function’s body for client-side WASM execution only.
hydrated_server
A wrapper around #[server] that automatically uses the zero-weight BrowserJson codec.
ssr
A macro to gate a function’s body for server-side execution only.