leptos/
suspense_component.rs

1use crate::{
2    children::{TypedChildren, ViewFnOnce},
3    error::ErrorBoundarySuspendedChildren,
4    IntoView,
5};
6use futures::{channel::oneshot, select, FutureExt};
7use hydration_context::SerializedDataId;
8use leptos_macro::component;
9use reactive_graph::{
10    computed::{
11        suspense::{LocalResourceNotifier, SuspenseContext},
12        ArcMemo, ScopedFuture,
13    },
14    effect::RenderEffect,
15    owner::{provide_context, use_context, Owner},
16    signal::ArcRwSignal,
17    traits::{Dispose, Get, Read, Track, With, WriteValue},
18};
19use slotmap::{DefaultKey, SlotMap};
20use std::sync::Arc;
21use tachys::{
22    either::Either,
23    html::attribute::{any_attribute::AnyAttribute, Attribute},
24    hydration::Cursor,
25    reactive_graph::{OwnedView, OwnedViewState},
26    ssr::StreamBuilder,
27    view::{
28        add_attr::AddAnyAttr,
29        either::{EitherKeepAlive, EitherKeepAliveState},
30        Mountable, Position, PositionState, Render, RenderHtml,
31    },
32};
33use throw_error::ErrorHookFuture;
34
35/// If any [`Resource`](crate::prelude::Resource) is read in the `children` of this
36/// component, it will show the `fallback` while they are loading. Once all are resolved,
37/// it will render the `children`.
38///
39/// Each time one of the resources is loading again, it will fall back. To keep the current
40/// children instead, use [Transition](crate::prelude::Transition).
41///
42/// Note that the `children` will be rendered initially (in order to capture the fact that
43/// those resources are read under the suspense), so you cannot assume that resources read
44/// synchronously have
45/// `Some` value in `children`. However, you can read resources asynchronously by using
46/// [Suspend](crate::prelude::Suspend).
47///
48/// ```
49/// # use leptos::prelude::*;
50/// # if false { // don't run in doctests
51/// async fn fetch_cats(how_many: u32) -> Vec<String> { vec![] }
52///
53/// let (cat_count, set_cat_count) = signal::<u32>(1);
54///
55/// let cats = Resource::new(move || cat_count.get(), |count| fetch_cats(count));
56///
57/// view! {
58///   <div>
59///     <Suspense fallback=move || view! { <p>"Loading (Suspense Fallback)..."</p> }>
60///       // you can access a resource synchronously
61///       {move || {
62///           cats.get().map(|data| {
63///             data
64///               .into_iter()
65///               .map(|src| {
66///                   view! {
67///                     <img src={src}/>
68///                   }
69///               })
70///               .collect_view()
71///           })
72///         }
73///       }
74///       // or you can use `Suspend` to read resources asynchronously
75///       {move || Suspend::new(async move {
76///         cats.await
77///               .into_iter()
78///               .map(|src| {
79///                   view! {
80///                     <img src={src}/>
81///                   }
82///               })
83///               .collect_view()
84///       })}
85///     </Suspense>
86///   </div>
87/// }
88/// # ;}
89/// ```
90#[component]
91pub fn Suspense<Chil>(
92    /// A function that returns a fallback that will be shown while resources are still loading.
93    /// By default this is an empty view.
94    #[prop(optional, into)]
95    fallback: ViewFnOnce,
96    /// Children will be rendered once initially to catch any resource reads, then hidden until all
97    /// data have loaded.
98    children: TypedChildren<Chil>,
99) -> impl IntoView
100where
101    Chil: IntoView + Send + 'static,
102{
103    let error_boundary_parent = use_context::<ErrorBoundarySuspendedChildren>();
104
105    let owner = Owner::new();
106    owner.with(|| {
107        let (starts_local, id) = {
108            Owner::current_shared_context()
109                .map(|sc| {
110                    let id = sc.next_id();
111                    (sc.get_incomplete_chunk(&id), id)
112                })
113                .unwrap_or_else(|| (false, Default::default()))
114        };
115        let fallback = fallback.run();
116        let children = children.into_inner()();
117        let tasks = ArcRwSignal::new(SlotMap::<DefaultKey, ()>::new());
118        provide_context(SuspenseContext {
119            tasks: tasks.clone(),
120        });
121        let none_pending = ArcMemo::new(move |prev: Option<&bool>| {
122            tasks.track();
123            if prev.is_none() && starts_local {
124                false
125            } else {
126                tasks.with(SlotMap::is_empty)
127            }
128        });
129
130        OwnedView::new(SuspenseBoundary::<false, _, _> {
131            id,
132            none_pending,
133            fallback,
134            children,
135            error_boundary_parent,
136        })
137    })
138}
139
140fn nonce_or_not() -> Option<Arc<str>> {
141    #[cfg(feature = "nonce")]
142    {
143        use crate::nonce::Nonce;
144        use_context::<Nonce>().map(|n| n.0)
145    }
146    #[cfg(not(feature = "nonce"))]
147    {
148        None
149    }
150}
151
152pub(crate) struct SuspenseBoundary<const TRANSITION: bool, Fal, Chil> {
153    pub id: SerializedDataId,
154    pub none_pending: ArcMemo<bool>,
155    pub fallback: Fal,
156    pub children: Chil,
157    pub error_boundary_parent: Option<ErrorBoundarySuspendedChildren>,
158}
159
160impl<const TRANSITION: bool, Fal, Chil> Render
161    for SuspenseBoundary<TRANSITION, Fal, Chil>
162where
163    Fal: Render + Send + 'static,
164    Chil: Render + Send + 'static,
165{
166    type State = RenderEffect<
167        OwnedViewState<EitherKeepAliveState<Chil::State, Fal::State>>,
168    >;
169
170    fn build(self) -> Self::State {
171        let mut children = Some(self.children);
172        let mut fallback = Some(self.fallback);
173        let none_pending = self.none_pending;
174        let mut nth_run = 0;
175        let outer_owner = Owner::new();
176
177        RenderEffect::new(move |prev| {
178            // show the fallback if
179            // 1) there are pending futures, and
180            // 2) we are either in a Suspense (not Transition), or it's the first fallback
181            //    (because we initially render the children to register Futures, the "first
182            //    fallback" is probably the 2nd run
183            let show_b = !none_pending.get() && (!TRANSITION || nth_run < 2);
184            nth_run += 1;
185            let this = OwnedView::new_with_owner(
186                EitherKeepAlive {
187                    a: children.take(),
188                    b: fallback.take(),
189                    show_b,
190                },
191                outer_owner.clone(),
192            );
193
194            if let Some(mut state) = prev {
195                this.rebuild(&mut state);
196                state
197            } else {
198                this.build()
199            }
200        })
201    }
202
203    fn rebuild(self, state: &mut Self::State) {
204        let new = self.build();
205        let mut old = std::mem::replace(state, new);
206        old.insert_before_this(state);
207        old.unmount();
208    }
209}
210
211impl<const TRANSITION: bool, Fal, Chil> AddAnyAttr
212    for SuspenseBoundary<TRANSITION, Fal, Chil>
213where
214    Fal: RenderHtml + Send + 'static,
215    Chil: RenderHtml + Send + 'static,
216{
217    type Output<SomeNewAttr: Attribute> = SuspenseBoundary<
218        TRANSITION,
219        Fal,
220        Chil::Output<SomeNewAttr::CloneableOwned>,
221    >;
222
223    fn add_any_attr<NewAttr: Attribute>(
224        self,
225        attr: NewAttr,
226    ) -> Self::Output<NewAttr>
227    where
228        Self::Output<NewAttr>: RenderHtml,
229    {
230        let attr = attr.into_cloneable_owned();
231        let SuspenseBoundary {
232            id,
233            none_pending,
234            fallback,
235            children,
236            error_boundary_parent,
237        } = self;
238        SuspenseBoundary {
239            id,
240            none_pending,
241            fallback,
242            children: children.add_any_attr(attr),
243            error_boundary_parent,
244        }
245    }
246}
247
248impl<const TRANSITION: bool, Fal, Chil> RenderHtml
249    for SuspenseBoundary<TRANSITION, Fal, Chil>
250where
251    Fal: RenderHtml + Send + 'static,
252    Chil: RenderHtml + Send + 'static,
253{
254    // i.e., if this is the child of another Suspense during SSR, don't wait for it: it will handle
255    // itself
256    type AsyncOutput = Self;
257    type Owned = Self;
258
259    const MIN_LENGTH: usize = Chil::MIN_LENGTH;
260
261    fn dry_resolve(&mut self) {}
262
263    async fn resolve(self) -> Self::AsyncOutput {
264        self
265    }
266
267    fn to_html_with_buf(
268        self,
269        buf: &mut String,
270        position: &mut Position,
271        escape: bool,
272        mark_branches: bool,
273        extra_attrs: Vec<AnyAttribute>,
274    ) {
275        self.fallback.to_html_with_buf(
276            buf,
277            position,
278            escape,
279            mark_branches,
280            extra_attrs,
281        );
282    }
283
284    fn to_html_async_with_buf<const OUT_OF_ORDER: bool>(
285        mut self,
286        buf: &mut StreamBuilder,
287        position: &mut Position,
288        escape: bool,
289        mark_branches: bool,
290        extra_attrs: Vec<AnyAttribute>,
291    ) where
292        Self: Sized,
293    {
294        buf.next_id();
295        let suspense_context = use_context::<SuspenseContext>().unwrap();
296        let owner = Owner::current().unwrap();
297
298        let mut notify_error_boundary =
299            self.error_boundary_parent.map(|children| {
300                let (tx, rx) = oneshot::channel();
301                children.write_value().push(rx);
302                tx
303            });
304
305        // we need to wait for one of two things: either
306        // 1. all tasks are finished loading, or
307        // 2. we read from a local resource, meaning this Suspense can never resolve on the server
308
309        // first, create listener for tasks
310        let tasks = suspense_context.tasks.clone();
311        let (tasks_tx, mut tasks_rx) =
312            futures::channel::oneshot::channel::<()>();
313
314        let mut tasks_tx = Some(tasks_tx);
315
316        // now, create listener for local resources
317        let (local_tx, mut local_rx) =
318            futures::channel::oneshot::channel::<()>();
319        provide_context(LocalResourceNotifier::from(local_tx));
320
321        // walk over the tree of children once to make sure that all resource loads are registered
322        self.children.dry_resolve();
323
324        // check the set of tasks to see if it is empty, now or later
325        let eff = reactive_graph::effect::Effect::new_isomorphic({
326            move |_| {
327                tasks.track();
328                if let Some(tasks) = tasks.try_read() {
329                    if tasks.is_empty() {
330                        if let Some(tx) = tasks_tx.take() {
331                            // If the receiver has dropped, it means the ScopedFuture has already
332                            // dropped, so it doesn't matter if we manage to send this.
333                            _ = tx.send(());
334                        }
335                        if let Some(tx) = notify_error_boundary.take() {
336                            _ = tx.send(());
337                        }
338                    }
339                }
340            }
341        });
342
343        let mut fut = Box::pin(ScopedFuture::new(ErrorHookFuture::new(
344            async move {
345                // race the local resource notifier against the set of tasks
346                //
347                // if there are local resources, we just return the fallback immediately
348                //
349                // otherwise, we want to wait for resources to load before trying to resolve the body
350                //
351                // this is *less efficient* than just resolving the body
352                // however, it means that you can use reactive accesses to resources/async derived
353                // inside component props, at any level, and have those picked up by Suspense, and
354                // that it will wait for those to resolve
355                select! {
356                    // if there are local resources, bail
357                    // this will only have fired by this point for local resources accessed
358                    // *synchronously*
359                    _ = local_rx => {
360                        let sc = Owner::current_shared_context().expect("no shared context");
361                        sc.set_incomplete_chunk(self.id);
362                        None
363                    }
364                    _ = tasks_rx => {
365                        // if we ran this earlier, reactive reads would always be registered as None
366                        // this is fine in the case where we want to use Suspend and .await on some future
367                        // but in situations like a <For each=|| some_resource.snapshot()/> we actually
368                        // want to be able to 1) synchronously read a resource's value, but still 2) wait
369                        // for it to load before we render everything
370                        let mut children = Box::pin(self.children.resolve().fuse());
371
372                        // we continue racing the children against the "do we have any local
373                        // resources?" Future
374                        select! {
375                            _ = local_rx => {
376                                let sc = Owner::current_shared_context().expect("no shared context");
377                                sc.set_incomplete_chunk(self.id);
378                                None
379                            }
380                            children = children => {
381                                // clean up the (now useless) effect
382                                eff.dispose();
383
384                                Some(OwnedView::new_with_owner(children, owner))
385                            }
386                        }
387                    }
388                }
389            },
390        )));
391        match fut.as_mut().now_or_never() {
392            Some(Some(resolved)) => {
393                Either::<Fal, _>::Right(resolved)
394                    .to_html_async_with_buf::<OUT_OF_ORDER>(
395                        buf,
396                        position,
397                        escape,
398                        mark_branches,
399                        extra_attrs,
400                    );
401            }
402            Some(None) => {
403                Either::<_, Chil>::Left(self.fallback)
404                    .to_html_async_with_buf::<OUT_OF_ORDER>(
405                        buf,
406                        position,
407                        escape,
408                        mark_branches,
409                        extra_attrs,
410                    );
411            }
412            None => {
413                let id = buf.clone_id();
414
415                // out-of-order streams immediately push fallback,
416                // wrapped by suspense markers
417                if OUT_OF_ORDER {
418                    let mut fallback_position = *position;
419                    buf.push_fallback(
420                        self.fallback,
421                        &mut fallback_position,
422                        mark_branches,
423                        extra_attrs.clone(),
424                    );
425                    buf.push_async_out_of_order_with_nonce(
426                        fut,
427                        position,
428                        mark_branches,
429                        nonce_or_not(),
430                        extra_attrs,
431                    );
432                } else {
433                    // calling this will walk over the tree, removing all event listeners
434                    // and other single-threaded values from the view tree. this needs to be
435                    // done because the fallback can be shifted to another thread in push_async below.
436                    self.fallback.dry_resolve();
437
438                    buf.push_async({
439                        let mut position = *position;
440                        async move {
441                            let value = match fut.await {
442                                None => Either::Left(self.fallback),
443                                Some(value) => Either::Right(value),
444                            };
445                            let mut builder = StreamBuilder::new(id);
446                            value.to_html_async_with_buf::<OUT_OF_ORDER>(
447                                &mut builder,
448                                &mut position,
449                                escape,
450                                mark_branches,
451                                extra_attrs,
452                            );
453                            builder.finish().take_chunks()
454                        }
455                    });
456                    *position = Position::NextChild;
457                }
458            }
459        };
460    }
461
462    fn hydrate<const FROM_SERVER: bool>(
463        self,
464        cursor: &Cursor,
465        position: &PositionState,
466    ) -> Self::State {
467        let cursor = cursor.to_owned();
468        let position = position.to_owned();
469
470        let mut children = Some(self.children);
471        let mut fallback = Some(self.fallback);
472        let none_pending = self.none_pending;
473        let mut nth_run = 0;
474        let outer_owner = Owner::new();
475
476        RenderEffect::new(move |prev| {
477            // show the fallback if
478            // 1) there are pending futures, and
479            // 2) we are either in a Suspense (not Transition), or it's the first fallback
480            //    (because we initially render the children to register Futures, the "first
481            //    fallback" is probably the 2nd run
482            let show_b = !none_pending.get() && (!TRANSITION || nth_run < 1);
483            nth_run += 1;
484            let this = OwnedView::new_with_owner(
485                EitherKeepAlive {
486                    a: children.take(),
487                    b: fallback.take(),
488                    show_b,
489                },
490                outer_owner.clone(),
491            );
492
493            if let Some(mut state) = prev {
494                this.rebuild(&mut state);
495                state
496            } else {
497                this.hydrate::<FROM_SERVER>(&cursor, &position)
498            }
499        })
500    }
501
502    fn into_owned(self) -> Self::Owned {
503        self
504    }
505}
506
507/// A wrapper that prevents [`Suspense`] from waiting for any resource reads that happen inside
508/// `Unsuspend`.
509pub struct Unsuspend<T>(Box<dyn FnOnce() -> T + Send>);
510
511impl<T> Unsuspend<T> {
512    /// Wraps the given function, such that it is not called until all resources are ready.
513    pub fn new(fun: impl FnOnce() -> T + Send + 'static) -> Self {
514        Self(Box::new(fun))
515    }
516}
517
518impl<T> Render for Unsuspend<T>
519where
520    T: Render,
521{
522    type State = T::State;
523
524    fn build(self) -> Self::State {
525        (self.0)().build()
526    }
527
528    fn rebuild(self, state: &mut Self::State) {
529        (self.0)().rebuild(state);
530    }
531}
532
533impl<T> AddAnyAttr for Unsuspend<T>
534where
535    T: AddAnyAttr + 'static,
536{
537    type Output<SomeNewAttr: Attribute> =
538        Unsuspend<T::Output<SomeNewAttr::CloneableOwned>>;
539
540    fn add_any_attr<NewAttr: Attribute>(
541        self,
542        attr: NewAttr,
543    ) -> Self::Output<NewAttr>
544    where
545        Self::Output<NewAttr>: RenderHtml,
546    {
547        let attr = attr.into_cloneable_owned();
548        Unsuspend::new(move || (self.0)().add_any_attr(attr))
549    }
550}
551
552impl<T> RenderHtml for Unsuspend<T>
553where
554    T: RenderHtml + 'static,
555{
556    type AsyncOutput = Self;
557    type Owned = Self;
558
559    const MIN_LENGTH: usize = T::MIN_LENGTH;
560
561    fn dry_resolve(&mut self) {}
562
563    async fn resolve(self) -> Self::AsyncOutput {
564        self
565    }
566
567    fn to_html_with_buf(
568        self,
569        buf: &mut String,
570        position: &mut Position,
571        escape: bool,
572        mark_branches: bool,
573        extra_attrs: Vec<AnyAttribute>,
574    ) {
575        (self.0)().to_html_with_buf(
576            buf,
577            position,
578            escape,
579            mark_branches,
580            extra_attrs,
581        );
582    }
583
584    fn to_html_async_with_buf<const OUT_OF_ORDER: bool>(
585        self,
586        buf: &mut StreamBuilder,
587        position: &mut Position,
588        escape: bool,
589        mark_branches: bool,
590        extra_attrs: Vec<AnyAttribute>,
591    ) where
592        Self: Sized,
593    {
594        (self.0)().to_html_async_with_buf::<OUT_OF_ORDER>(
595            buf,
596            position,
597            escape,
598            mark_branches,
599            extra_attrs,
600        );
601    }
602
603    fn hydrate<const FROM_SERVER: bool>(
604        self,
605        cursor: &Cursor,
606        position: &PositionState,
607    ) -> Self::State {
608        (self.0)().hydrate::<FROM_SERVER>(cursor, position)
609    }
610
611    fn into_owned(self) -> Self::Owned {
612        self
613    }
614}