leptos_sync_ssr/lib.rs
1//! This crate provides helpers to synchronize the access of Leptos resources
2//! during server-side rendering (SSR) within the Leptos frameworks. This is
3//! achieved by providing locking primitives, which are then built into the
4//! provided components and signals, such that when they are used together in
5//! the prescribed manner, it would allow the affected resources to resolve in
6//! the expected order. This enables components defined earlier in the view
7//! tree to wait on data that may or may not be provided by components defined
8//! later down the view tree, ultimately ensuring that hydration would happen
9//! correctly.
10//!
11//! ## Use case
12//!
13//! A fairly common user interface design pattern known as [portlets](
14//! https://en.wikipedia.org/wiki/Portlet) may be implemented in Leptos
15//! using a struct with data fields to be rendered be available in through a
16//! reactive signal behind a context, with the renderer being a component
17//! that would reactively read the value from that reactive signal such that
18//! it may be updated as required. This is not a problem under client-side
19//! rendering (CSR) for Leptos, but for SSR with hydration, this is a whole
20//! other story.
21//!
22//! If the rendering component is situated in the view tree before the
23//! component that may write to it, as in the case of a typical "breadcrumb"
24//! component, this creates the complication where the signal may not be set
25//! in time by the time the breadcrumb component was streamed out to the
26//! client with the default data. Furthermore, the hydration script may
27//! contain the expected data, and when used to hydrate the rendered markup
28//! which used the default data from earlier, this mismatch wil result in
29//! hydration error.
30//!
31//! There are multiple solutions to this problem, and this crate provides a
32//! number of them, when combined together, addresses the rendering and
33//! hydration issues, without bringing in new problems that the individual
34//! solution would bring when used in isolation.
35//!
36//! # Example
37//!
38//! This package does in fact implement [`portlets`](crate::portlet) as a
39//! module, where a minimum amount of user code is required to set up the
40//! scenario as described above. The following is an example on how that
41//! module may be used:
42//!
43//! ```
44//! use leptos::prelude::*;
45//! use leptos_router::{
46//! components::{Route, Router, Routes},
47//! path,
48//! };
49//! use leptos_sync_ssr::{component::SyncSsrSignal, portlet::PortletCtx};
50//! #
51//! # #[derive(serde::Serialize, serde::Deserialize, Clone, Debug, PartialEq)]
52//! # struct Blog;
53//!
54//! #[derive(serde::Serialize, serde::Deserialize, Clone, Debug, PartialEq)]
55//! struct Breadcrumbs {
56//! // fields...
57//! }
58//!
59//! // The main `App` - note how the `SyncSsrSignal` is placed.
60//! #[component]
61//! fn App() -> impl IntoView {
62//! view! {
63//! <Router>
64//! <SyncSsrSignal setup=|| {
65//! <PortletCtx<Breadcrumbs>>::provide();
66//! }>
67//! <header>
68//! // Note how the breadcrumb comes before anything that
69//! // might set the signal for it.
70//! <ShowBreadcrumbs/>
71//! </header>
72//! <article>
73//! <Routes fallback=|| ()>
74//! <Route path=path!("") view=HomePage/>
75//! <Route path=path!("/blog/:id") view=BlogView/>
76//! // plus other routes
77//! </Routes>
78//! </article>
79//! </SyncSsrSignal>
80//! </Router>
81//! }
82//! }
83//!
84//! // This implements the rendering for `BreadCrumbs`
85//! impl IntoRender for Breadcrumbs {
86//! type Output = AnyView;
87//!
88//! fn into_render(self) -> Self::Output {
89//! view! {
90//! todo!()
91//! }
92//! .into_any()
93//! }
94//! }
95//!
96//! #[component]
97//! fn ShowBreadcrumbs() -> impl IntoView {
98//! <PortletCtx<Breadcrumbs>>::render()
99//! }
100//!
101//! // This renders the blog, but also sets the breadcrumbs as appropriate.
102//! #[component]
103//! fn BlogView() -> impl IntoView {
104//! // assuming this was provided/defined elsewhere
105//! let blog = expect_context::<ArcResource<Result<Blog, ServerFnError>>>();
106//! let nav_ctx = expect_context::<PortletCtx<Breadcrumbs>>();
107//!
108//! view! {
109//! // This ensures `PortletCtx<Breadcrumbs>` is updated with data provided by
110//! // `authors`.
111//! {nav_ctx.set_with(move || {
112//! let blog = blog.clone();
113//! async move {
114//! blog.await
115//! // TODO conversion of the blog info to `Breadcrumbs` type
116//! .map(|blog| todo!())
117//! .ok()
118//! // Once this returns, the breadcrumb will render on SSR.
119//! }
120//! })}
121//! <div>
122//! // Other components/elements.
123//! </div>
124//! }
125//! }
126//!
127//! #[component]
128//! fn HomePage() -> impl IntoView {
129//! // Note how the homepage doesn't set breadcrumbs - it then shouldn't
130//! // show the breadcrumbs, and that lack of write signal creation/use
131//! // wouldn't cause a deadlock.
132//! view! {
133//! todo!()
134//! }
135//! }
136//! ```
137//!
138//! For a more complete example on something similar to above, [`nav_portlet`](
139//! https://github.com/metatoaster/leptos_sync_ssr/tree/main/example/nav_portlet)
140//! provides a great demonstration on how this might work in action, and
141//! that there is also an alternative implementation called [`nav_portlet_alt`](
142//! https://github.com/metatoaster/leptos_sync_ssr/tree/main/example/nav_portlet_alt)
143//! that use the other set of primitives to achieve a similar effect.
144//! The documentation under individual modules and types also include further
145//! explanation, usage examples, and does go more into the implementation details,
146//! please check them out.
147//!
148//! # Feature Flags
149#![cfg_attr(
150 feature = "document-features",
151 cfg_attr(doc, doc = ::document_features::document_features!())
152)]
153
154pub mod component;
155#[cfg(feature = "portlet")]
156pub mod portlet;
157mod ready;
158pub mod signal;
159
160#[cfg(test)]
161mod tests;
162
163pub use ready::{
164 CoReady, CoReadyCoordinator, CoReadySubscription, Ready, ReadyHandle, ReadySubscription,
165};