#resources #component #version

leptos_sync_ssr

A helper for the Leptos Rust web framework for synchronizing server-side resources

2 releases

new 0.1.0-beta2 Mar 25, 2025
0.1.0-beta Mar 24, 2025

#651 in HTTP server

Download history 163/week @ 2025-03-21

163 downloads per month

MIT license

35KB
330 lines

leptos_sync_ssr

leptos_sync_ssr provides an additional component and primitive that would aid in synchronize access of Leptos resource during server-side rendering (SSR) within the Leptos integration frameworks.

Introduction

A fairly common user interface design pattern, where a common reusable element or component situated earlier (that is, placed somewhere before the other) depending on resources provided by some later component. Examples of such designs include breadcrumbs and portlets.

This pattern is supported well under Leptos for CSR, but under SSR + hydration, this pattern is barely supported, if at all. This is simply due to how signals and resources are resolve in the order they are defined, and once they are resolved, they may get sent down to the client, possibly without the intended resource that would produce the desired content set.

One approach to address this issue is to provide an additional server- side only component that will provide a broadcast channel that resources may listen and wait for, such that only after all the relevant processing is completed that a signal will be sent to allow those withheld resources to continue processing. This additional delay will ensure the correct values be read and the intended output be produced.

Example

As an example, here's a component using some resource that may be set later through a signal:

#[component]
pub fn UsingSignal() -> impl IntoView {
    let rs = expect_context::<ReadSignal<Option<OnceResource<String>>>>();
    let ready = Ready::handle();
    let value = Resource::new_blocking(
        || (),
        move |_| {
            let ready = ready.clone();
            async move {
                // This ensures the async closure will wait until the
                // ready signal is received before trying to read from
                // the signal to access the resource.  Moreover, the
                // implementation only functions under SSR, despite the
                // lack of feature gating here as a dummy no-op version
                // is provided for CSR.  Refer to examples for more
                // documentation on this.
                ready.subscribe().wait().await;
                if let Some(Some(res)) = rs.try_get() {
                    Some(res.await)
                } else {
                    None
                }
            }
        },
    );
    view! {
        <p>
            <span>"The content is: "</span>
            <Suspense>{
                move || Suspend::new(async move {
                    value.await.map(|value| {
                        view! {
                            <strong>{value}</strong>
                        }
                    })
                })
            }</Suspense>
        </p>
    }
}

Where the usage of the write signal may occur some time after this. The second part is to enclose the affected components, i.e. the reader and all the possible writers, inside the <SyncSsr> component. Typically this may be done at inside the <App>, e.g.:

    view! {
        <Router>
            <header>
                <nav>
                    // link to routes...
                </nav>
            </header>
            <main>
                <SyncSsr>
                    <UsingSignal/>
                    <Routes fallback>
                        <Route path=path!("") view=HomePage/>
                        <RoutesThatMaySetSignal/>
                    </Routes>
                </SyncSsr>
            </main>
        </Router>
    }

The usage of <SyncSsr> component is not just limited to the top level App, as it uses the <Provider> component underneath to scope Ready to where it's required. Refer to the simple example this scoped example, and for a more practical and complete example, refer to the nav_portlet example.

Supported Leptos version

In order for the typical patterns as described to function, this package requires bug fixes to Leptos that will land in 0.8.0-beta, hence that would be the minimum Leptos version. While the core functionality provided by this package can work under 0.7, just that no releases will be made to depend on that as it will lack bug fixes, hences bugs that will prevent the examples provided with this package from working.

Usage

To use leptos_sync_ssr, add it to Cargo.toml, and use the ssr feature as per convention:

[dependencies]
leptos_sync_ssr = "0.1.0-beta"
leptos = "0.8.0-beta"

[features]
hydrate = [
  "leptos/hydrate",
]
# the app will need the ssr feature from leptos_sync_ssr
ssr = [
  "leptos/ssr",
  "leptos_sync_ssr/ssr",
]

A more complete example Cargo.toml from the simple example.

Alternative packages, solutions and limitations

The approach provided by this package is certainly not the only option for passing values asynchronously to an earlier component. One alternative is to follow the approach taken by leptos_async_signal. That package provides a mechanism for generating values asynchronously, it claims to mimic the approach taken by leptos_meta, however, it does require the AsyncWriteSignal be used if the paired ArcResource were to be read, otherwise a deadlock will ensure, The particular issue about unused signals causing deadlocks may be addressed should this pull request be merged. However, there are other rules that must be followed to avoid deadlocks, so extra care must be taken to use its async_signal correctly.

On the other hand, leptos_sync_ssr does not have such limitations - the waiting can happen inside a Suspend, just that it may be better to have the wait done in Resource simply due to how Leptos SSR will always poll Resource unlike Suspend. Waiting inside the Suspend will have somewhat more variations and thus having somewhat lower reliability of this working correctly under a work-stealing task scheduler.

That being said, the approach taken by leptos_async_signal is much more granular and has the bonus of being fully unaffected by the interactions between work-stealing scheduler and how Leptos handles the Suspend and resource futures. Tested using 0.8.0-beta and with 200k requests (5 concurrent). Whereas the solution provided with leptos_sync_ssr merely extends on the existing features so the issues of that interaction will still apply. In 100k requests, up to 10 requests may have an unexpected output which may or may not affect hydration, although this may simply caused by a lack of synchronization in Leptos itself when running inside a work-stealing task scheduler. A discussion of the underlying topic at leptos/leptos-rs#3729 currently documents my findings with the particular pattern I've used.

License

This package is provided under the MIT license.

Dependencies

~22–35MB
~566K SLoC