Skip to main content

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