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}