#leptos #html #hydrate

leptos-posthoc

A crate for dynamically hydrating static/opaque HTML using leptos components

1 unstable release

Uses new Rust 2024

0.1.0 Jun 4, 2025

#1882 in HTTP server

GPL-3.0-or-later

65KB
722 lines

leptos posthoc

Allows for "hydrating" an existent DOM with reactive leptos components, without the entire DOM having to be generated by leptos components.

Why would you want that?

  1. CSR: It allows for building scripts that others can just embed in their arbitrary HTML documents, that add <insert your favourite fancy feature here>. For an example, see the examples/csr directory: the index.html has a node <script src='csr_example.js'></script>, which "hydrates" nodes with the data-replace-with-leptos-attribute with leptos components that add a hover-popup (using thaw).
  2. SSR: Occasionally, you might want to dynamically insert some HTML string into the DOM, for example one that gets generated from some data and returned by a server function. This HTML might contain certain nodes that we want to attach reactive functionality to. For an example, see the examples/ssr directory.

CSR Example

Say we want to replace all elements with the attribute data-replace-with-leptos with a leptos component MyReplacementComponent, that simply wraps the original children in a div with a solid red border. This component would roughly look like this:

#[component]
fn MyReplacementComponent(orig:OriginalNode) -> impl IntoView {
   view! {
      <div style="border: 1px solid red;">
        <DomChildren orig />
     </div>
  }
}

This component takes an orig:OriginalNode that represents the, well, original Element.

So, where do we get orig from?

  • If we already have an e:&Element, we can simply call e.into().

  • More likely, we don't have an Element yet. Moreover, we probably want to iterate over the entire body once to find all nodes we want to make reactive, and we also need to set up a global reactive system for all our inserted components.

    To do that, we call hydrate_body (requires the csr feature flag) with a function that takes the OriginalNode of the body and returns some leptos view; e.g.:

#[component]
fn MainBody(orig:OriginalNode) -> impl IntoView {
  // set up some signals, provide context etc.
  view!{
    <DomChildren orig/>
  }
}
#[wasm_bindgen(start)]
pub fn run() {
  console_error_panic_hook::set_once();
  hydrate_body(|orig| view!(<MainBody orig/>).into_any())
}

This sets up the reactive system, but does not yet replace any elements further down in the DOM. To do that, we provide a function that takes an &Element and optionally returns an FnOnce() -> impl IntoView+'static, if the element should be changed. This function is then passed to DomChildrenCont, which will iterate over all children of the replaced element and replace them with the provided function.

Let's modify our MainBody to replace all elements with the attribute data-replace-with-leptos with a MyReplacementComponent:

fn replace(e:&Element) -> Option<impl FnOnce() -> AnyView> {
  e.get_attribute("data-replace-with-leptos").map(|_| {
    let orig: OriginalNode = e.clone().into();
    || view!(<MyReplacementComponent orig/>).into_any()
  })
}

#[component]
fn MainBody(orig:OriginalNode) -> impl IntoView {
  // set up some signals, provide context etc.
  view!{
    <DomChildrenCont orig cont=replace/>
  }
}

#[component]
fn MyReplacementComponent(orig:OriginalNode) -> impl IntoView {
  view! {
    <div style="border: 1px solid red;">
      <DomChildrenCont orig cont=replace/>
    </div>
  }
}

...now, replace will get called on every element of the DOM, including those that were "moved around" in earlier MyReplacementComponents, respecting the proper reactive graph (regardin signal inheritance etc.).

SSR Example

In general, for SSR we can simply use the normal leptos components to generate the entire DOM. We control the server, hence we control the DOM anyway.

However, it might occasionally be the case that we want to dynamically extend the DOM at some point by retrieving HTML from elsewhere, and then want to do a similar "hydration" iteration over the freshly inserted nodes. This is what DomStringCont is for, and it does not require the csr feature:

#[component]
fn MyComponentThatGetsAStringFromSomewhere() -> impl IntoView {
  // get some HTML string from somewhere
  // e.g. some API call
  let html = "<div data-replace-with-leptos>...</div>".to_string();
  view! {
    <DomStringCont html cont=replace/>
  }
}

See the examples/ssr directory for a full example.

Dependencies

~23–31MB
~581K SLoC