dioxus_core/suspense/
component.rs

1use crate::{innerlude::*, scope_context::SuspenseLocation};
2
3/// Properties for the [`SuspenseBoundary()`] component.
4#[allow(non_camel_case_types)]
5pub struct SuspenseBoundaryProps {
6    fallback: Callback<SuspenseContext, Element>,
7    /// The children of the suspense boundary
8    children: Element,
9}
10
11impl Clone for SuspenseBoundaryProps {
12    fn clone(&self) -> Self {
13        Self {
14            fallback: self.fallback,
15            children: self.children.clone(),
16        }
17    }
18}
19
20impl SuspenseBoundaryProps {
21    /**
22    Create a builder for building `SuspenseBoundaryProps`.
23    On the builder, call `.fallback(...)`, `.children(...)`(optional) to set the values of the fields.
24    Finally, call `.build()` to create the instance of `SuspenseBoundaryProps`.
25                        */
26    #[allow(dead_code, clippy::type_complexity)]
27    fn builder() -> SuspenseBoundaryPropsBuilder<((), ())> {
28        SuspenseBoundaryPropsBuilder {
29            owner: Owner::default(),
30            fields: ((), ()),
31            _phantom: ::core::default::Default::default(),
32        }
33    }
34}
35#[must_use]
36#[doc(hidden)]
37#[allow(dead_code, non_camel_case_types, non_snake_case)]
38pub struct SuspenseBoundaryPropsBuilder<TypedBuilderFields> {
39    owner: Owner,
40    fields: TypedBuilderFields,
41    _phantom: (),
42}
43impl Properties for SuspenseBoundaryProps
44where
45    Self: Clone,
46{
47    type Builder = SuspenseBoundaryPropsBuilder<((), ())>;
48    fn builder() -> Self::Builder {
49        SuspenseBoundaryProps::builder()
50    }
51    fn memoize(&mut self, new: &Self) -> bool {
52        let equal = self == new;
53        self.fallback.__point_to(&new.fallback);
54        if !equal {
55            let new_clone = new.clone();
56            self.children = new_clone.children;
57        }
58        equal
59    }
60}
61#[doc(hidden)]
62#[allow(dead_code, non_camel_case_types, non_snake_case)]
63pub trait SuspenseBoundaryPropsBuilder_Optional<T> {
64    fn into_value<F: FnOnce() -> T>(self, default: F) -> T;
65}
66impl<T> SuspenseBoundaryPropsBuilder_Optional<T> for () {
67    fn into_value<F: FnOnce() -> T>(self, default: F) -> T {
68        default()
69    }
70}
71impl<T> SuspenseBoundaryPropsBuilder_Optional<T> for (T,) {
72    fn into_value<F: FnOnce() -> T>(self, _: F) -> T {
73        self.0
74    }
75}
76#[allow(dead_code, non_camel_case_types, missing_docs)]
77impl<__children> SuspenseBoundaryPropsBuilder<((), __children)> {
78    #[allow(clippy::type_complexity)]
79    pub fn fallback<__Marker>(
80        self,
81        fallback: impl SuperInto<Callback<SuspenseContext, Element>, __Marker>,
82    ) -> SuspenseBoundaryPropsBuilder<((Callback<SuspenseContext, Element>,), __children)> {
83        let fallback = (with_owner(self.owner.clone(), move || {
84            SuperInto::super_into(fallback)
85        }),);
86        let (_, children) = self.fields;
87        SuspenseBoundaryPropsBuilder {
88            owner: self.owner,
89            fields: (fallback, children),
90            _phantom: self._phantom,
91        }
92    }
93}
94#[doc(hidden)]
95#[allow(dead_code, non_camel_case_types, non_snake_case)]
96pub enum SuspenseBoundaryPropsBuilder_Error_Repeated_field_fallback {}
97#[doc(hidden)]
98#[allow(dead_code, non_camel_case_types, missing_docs)]
99impl<__children> SuspenseBoundaryPropsBuilder<((Callback<SuspenseContext, Element>,), __children)> {
100    #[deprecated(note = "Repeated field fallback")]
101    #[allow(clippy::type_complexity)]
102    pub fn fallback(
103        self,
104        _: SuspenseBoundaryPropsBuilder_Error_Repeated_field_fallback,
105    ) -> SuspenseBoundaryPropsBuilder<((Callback<SuspenseContext, Element>,), __children)> {
106        self
107    }
108}
109#[allow(dead_code, non_camel_case_types, missing_docs)]
110impl<__fallback> SuspenseBoundaryPropsBuilder<(__fallback, ())> {
111    #[allow(clippy::type_complexity)]
112    pub fn children(
113        self,
114        children: Element,
115    ) -> SuspenseBoundaryPropsBuilder<(__fallback, (Element,))> {
116        let children = (children,);
117        let (fallback, _) = self.fields;
118        SuspenseBoundaryPropsBuilder {
119            owner: self.owner,
120            fields: (fallback, children),
121            _phantom: self._phantom,
122        }
123    }
124}
125#[doc(hidden)]
126#[allow(dead_code, non_camel_case_types, non_snake_case)]
127pub enum SuspenseBoundaryPropsBuilder_Error_Repeated_field_children {}
128#[doc(hidden)]
129#[allow(dead_code, non_camel_case_types, missing_docs)]
130impl<__fallback> SuspenseBoundaryPropsBuilder<(__fallback, (Element,))> {
131    #[deprecated(note = "Repeated field children")]
132    #[allow(clippy::type_complexity)]
133    pub fn children(
134        self,
135        _: SuspenseBoundaryPropsBuilder_Error_Repeated_field_children,
136    ) -> SuspenseBoundaryPropsBuilder<(__fallback, (Element,))> {
137        self
138    }
139}
140#[doc(hidden)]
141#[allow(dead_code, non_camel_case_types, non_snake_case)]
142pub enum SuspenseBoundaryPropsBuilder_Error_Missing_required_field_fallback {}
143#[doc(hidden)]
144#[allow(dead_code, non_camel_case_types, missing_docs, clippy::panic)]
145impl<__children> SuspenseBoundaryPropsBuilder<((), __children)> {
146    #[deprecated(note = "Missing required field fallback")]
147    pub fn build(
148        self,
149        _: SuspenseBoundaryPropsBuilder_Error_Missing_required_field_fallback,
150    ) -> SuspenseBoundaryProps {
151        panic!()
152    }
153}
154#[doc(hidden)]
155#[allow(dead_code, non_camel_case_types, missing_docs)]
156pub struct SuspenseBoundaryPropsWithOwner {
157    inner: SuspenseBoundaryProps,
158    owner: Owner,
159}
160#[automatically_derived]
161#[allow(dead_code, non_camel_case_types, missing_docs)]
162impl ::core::clone::Clone for SuspenseBoundaryPropsWithOwner {
163    #[inline]
164    fn clone(&self) -> SuspenseBoundaryPropsWithOwner {
165        SuspenseBoundaryPropsWithOwner {
166            inner: ::core::clone::Clone::clone(&self.inner),
167            owner: ::core::clone::Clone::clone(&self.owner),
168        }
169    }
170}
171impl PartialEq for SuspenseBoundaryPropsWithOwner {
172    fn eq(&self, other: &Self) -> bool {
173        self.inner.eq(&other.inner)
174    }
175}
176impl SuspenseBoundaryPropsWithOwner {
177    /// Create a component from the props.
178    pub fn into_vcomponent<M: 'static>(
179        self,
180        render_fn: impl ComponentFunction<SuspenseBoundaryProps, M>,
181    ) -> VComponent {
182        let component_name = std::any::type_name_of_val(&render_fn);
183        VComponent::new(
184            move |wrapper: Self| render_fn.rebuild(wrapper.inner),
185            self,
186            component_name,
187        )
188    }
189}
190impl Properties for SuspenseBoundaryPropsWithOwner {
191    type Builder = ();
192    fn builder() -> Self::Builder {
193        unreachable!()
194    }
195    fn memoize(&mut self, new: &Self) -> bool {
196        self.inner.memoize(&new.inner)
197    }
198}
199#[allow(dead_code, non_camel_case_types, missing_docs)]
200impl<__children: SuspenseBoundaryPropsBuilder_Optional<Element>>
201    SuspenseBoundaryPropsBuilder<((Callback<SuspenseContext, Element>,), __children)>
202{
203    pub fn build(self) -> SuspenseBoundaryPropsWithOwner {
204        let (fallback, children) = self.fields;
205        let fallback = fallback.0;
206        let children = SuspenseBoundaryPropsBuilder_Optional::into_value(children, VNode::empty);
207        SuspenseBoundaryPropsWithOwner {
208            inner: SuspenseBoundaryProps { fallback, children },
209            owner: self.owner,
210        }
211    }
212}
213#[automatically_derived]
214#[allow(non_camel_case_types)]
215impl ::core::cmp::PartialEq for SuspenseBoundaryProps {
216    #[inline]
217    fn eq(&self, other: &SuspenseBoundaryProps) -> bool {
218        self.fallback == other.fallback && self.children == other.children
219    }
220}
221
222/// Suspense Boundaries let you render a fallback UI while a child component is suspended.
223///
224/// # Example
225///
226/// ```rust
227/// # use dioxus::prelude::*;
228/// # fn Article() -> Element { rsx! { "Article" } }
229/// fn App() -> Element {
230///     rsx! {
231///         SuspenseBoundary {
232///             fallback: |context: SuspenseContext| rsx! {
233///                 if let Some(placeholder) = context.suspense_placeholder() {
234///                     {placeholder}
235///                 } else {
236///                     "Loading..."
237///                 }
238///             },
239///             Article {}
240///         }
241///     }
242/// }
243/// ```
244#[allow(non_snake_case)]
245pub fn SuspenseBoundary(mut __props: SuspenseBoundaryProps) -> Element {
246    unreachable!("SuspenseBoundary should not be called directly")
247}
248#[allow(non_snake_case)]
249#[doc(hidden)]
250mod SuspenseBoundary_completions {
251    #[doc(hidden)]
252    #[allow(non_camel_case_types)]
253    /// This enum is generated to help autocomplete the braces after the component. It does nothing
254    pub enum Component {
255        SuspenseBoundary {},
256    }
257}
258use generational_box::Owner;
259#[allow(unused)]
260pub use SuspenseBoundary_completions::Component::SuspenseBoundary;
261
262/// Suspense has a custom diffing algorithm that diffs the suspended nodes in the background without rendering them
263impl SuspenseBoundaryProps {
264    /// Try to downcast [`AnyProps`] to [`SuspenseBoundaryProps`]
265    pub(crate) fn downcast_from_props(props: &mut dyn AnyProps) -> Option<&mut Self> {
266        let inner: Option<&mut SuspenseBoundaryPropsWithOwner> = props.props_mut().downcast_mut();
267        inner.map(|inner| &mut inner.inner)
268    }
269
270    pub(crate) fn create<M: WriteMutations>(
271        mount: MountId,
272        idx: usize,
273        component: &VComponent,
274        parent: Option<ElementRef>,
275        dom: &mut VirtualDom,
276        to: Option<&mut M>,
277    ) -> usize {
278        let mut scope_id = ScopeId(dom.get_mounted_dyn_node(mount, idx));
279        // If the ScopeId is a placeholder, we need to load up a new scope for this vcomponent. If it's already mounted, then we can just use that
280        if scope_id.is_placeholder() {
281            {
282                let suspense_context = SuspenseContext::new();
283
284                let suspense_boundary_location =
285                    crate::scope_context::SuspenseLocation::SuspenseBoundary(
286                        suspense_context.clone(),
287                    );
288                dom.runtime
289                    .clone()
290                    .with_suspense_location(suspense_boundary_location, || {
291                        let scope_state = dom
292                            .new_scope(component.props.duplicate(), component.name)
293                            .state();
294                        suspense_context.mount(scope_state.id);
295                        scope_id = scope_state.id;
296                    });
297            }
298
299            // Store the scope id for the next render
300            dom.set_mounted_dyn_node(mount, idx, scope_id.0);
301        }
302        dom.runtime.clone().with_scope_on_stack(scope_id, || {
303            let scope_state = &mut dom.scopes[scope_id.0];
304            let props = Self::downcast_from_props(&mut *scope_state.props).unwrap();
305            let suspense_context =
306                SuspenseContext::downcast_suspense_boundary_from_scope(&dom.runtime, scope_id)
307                    .unwrap();
308
309            let children = props.children.clone();
310
311            // First always render the children in the background. Rendering the children may cause this boundary to suspend
312            suspense_context.under_suspense_boundary(&dom.runtime(), || {
313                children.as_vnode().create(dom, parent, None::<&mut M>);
314            });
315
316            // Store the (now mounted) children back into the scope state
317            let scope_state = &mut dom.scopes[scope_id.0];
318            let props = Self::downcast_from_props(&mut *scope_state.props).unwrap();
319            props.children.clone_from(&children);
320
321            let scope_state = &mut dom.scopes[scope_id.0];
322            let suspense_context = scope_state
323                .state()
324                .suspense_location()
325                .suspense_context()
326                .unwrap()
327                .clone();
328            // If there are suspended futures, render the fallback
329            let nodes_created = if !suspense_context.suspended_futures().is_empty() {
330                let (node, nodes_created) =
331                    suspense_context.in_suspense_placeholder(&dom.runtime(), || {
332                        let scope_state = &mut dom.scopes[scope_id.0];
333                        let props = Self::downcast_from_props(&mut *scope_state.props).unwrap();
334                        let suspense_context =
335                            SuspenseContext::downcast_suspense_boundary_from_scope(
336                                &dom.runtime,
337                                scope_id,
338                            )
339                            .unwrap();
340                        suspense_context.set_suspended_nodes(children.into());
341                        let suspense_placeholder = props.fallback.call(suspense_context);
342                        let nodes_created = suspense_placeholder.as_vnode().create(dom, parent, to);
343                        (suspense_placeholder, nodes_created)
344                    });
345
346                let scope_state = &mut dom.scopes[scope_id.0];
347                scope_state.last_rendered_node = Some(node);
348
349                nodes_created
350            } else {
351                // Otherwise just render the children in the real dom
352                debug_assert!(children.as_vnode().mount.get().mounted());
353                let nodes_created = suspense_context
354                    .under_suspense_boundary(&dom.runtime(), || {
355                        children.as_vnode().create(dom, parent, to)
356                    });
357                let scope_state = &mut dom.scopes[scope_id.0];
358                scope_state.last_rendered_node = Some(children);
359                let suspense_context =
360                    SuspenseContext::downcast_suspense_boundary_from_scope(&dom.runtime, scope_id)
361                        .unwrap();
362                suspense_context.take_suspended_nodes();
363                mark_suspense_resolved(dom, scope_id);
364
365                nodes_created
366            };
367            nodes_created
368        })
369    }
370
371    #[doc(hidden)]
372    /// Manually rerun the children of this suspense boundary without diffing against the old nodes.
373    ///
374    /// This should only be called by dioxus-web after the suspense boundary has been streamed in from the server.
375    pub fn resolve_suspense<M: WriteMutations>(
376        scope_id: ScopeId,
377        dom: &mut VirtualDom,
378        to: &mut M,
379        only_write_templates: impl FnOnce(&mut M),
380        replace_with: usize,
381    ) {
382        dom.runtime.clone().with_scope_on_stack(scope_id, || {
383            let _runtime = RuntimeGuard::new(dom.runtime());
384            let Some(scope_state) = dom.scopes.get_mut(scope_id.0) else {
385                return;
386            };
387
388            // Reset the suspense context
389            let suspense_context = scope_state
390                .state()
391                .suspense_location()
392                .suspense_context()
393                .unwrap()
394                .clone();
395            suspense_context.inner.suspended_tasks.borrow_mut().clear();
396
397            // Get the parent of the suspense boundary to later create children with the right parent
398            let currently_rendered = scope_state.last_rendered_node.as_ref().unwrap().clone();
399            let mount = currently_rendered.as_vnode().mount.get();
400            let parent = {
401                let mounts = dom.runtime.mounts.borrow();
402                mounts
403                    .get(mount.0)
404                    .expect("suspense placeholder is not mounted")
405                    .parent
406            };
407
408            let props = Self::downcast_from_props(&mut *scope_state.props).unwrap();
409
410            // Unmount any children to reset any scopes under this suspense boundary
411            let children = props.children.clone();
412            let suspense_context =
413                SuspenseContext::downcast_suspense_boundary_from_scope(&dom.runtime, scope_id)
414                    .unwrap();
415            // Take the suspended nodes out of the suspense boundary so the children know that the boundary is not suspended while diffing
416            let suspended = suspense_context.take_suspended_nodes();
417            if let Some(node) = suspended {
418                node.remove_node(&mut *dom, None::<&mut M>, None);
419            }
420            // Replace the rendered nodes with resolved nodes
421            currently_rendered
422                .as_vnode()
423                .remove_node(&mut *dom, Some(to), Some(replace_with));
424
425            // Switch to only writing templates
426            only_write_templates(to);
427
428            children.as_vnode().mount.take();
429
430            // First always render the children in the background. Rendering the children may cause this boundary to suspend
431            suspense_context.under_suspense_boundary(&dom.runtime(), || {
432                children.as_vnode().create(dom, parent, Some(to));
433            });
434
435            // Store the (now mounted) children back into the scope state
436            let scope_state = &mut dom.scopes[scope_id.0];
437            let props = Self::downcast_from_props(&mut *scope_state.props).unwrap();
438            props.children.clone_from(&children);
439            scope_state.last_rendered_node = Some(children);
440        })
441    }
442
443    pub(crate) fn diff<M: WriteMutations>(
444        scope_id: ScopeId,
445        dom: &mut VirtualDom,
446        to: Option<&mut M>,
447    ) {
448        dom.runtime.clone().with_scope_on_stack(scope_id, || {
449            let scope = &mut dom.scopes[scope_id.0];
450            let myself = Self::downcast_from_props(&mut *scope.props)
451                .unwrap()
452                .clone();
453
454            let last_rendered_node = scope.last_rendered_node.as_ref().unwrap().clone();
455
456            let Self {
457                fallback, children, ..
458            } = myself;
459
460            let suspense_context = scope.state().suspense_boundary().unwrap().clone();
461            let suspended_nodes = suspense_context.suspended_nodes();
462            let suspended = !suspense_context.suspended_futures().is_empty();
463            match (suspended_nodes, suspended) {
464                // We already have suspended nodes that still need to be suspended
465                // Just diff the normal and suspended nodes
466                (Some(suspended_nodes), true) => {
467                    let new_suspended_nodes: VNode = children.into();
468
469                    // Diff the placeholder nodes in the dom
470                    let new_placeholder =
471                        suspense_context.in_suspense_placeholder(&dom.runtime(), || {
472                            let old_placeholder = last_rendered_node;
473                            let new_placeholder = fallback.call(suspense_context.clone());
474
475                            old_placeholder.as_vnode().diff_node(
476                                new_placeholder.as_vnode(),
477                                dom,
478                                to,
479                            );
480                            new_placeholder
481                        });
482
483                    // Set the last rendered node to the placeholder
484                    dom.scopes[scope_id.0].last_rendered_node = Some(new_placeholder);
485
486                    // Diff the suspended nodes in the background
487                    suspense_context.under_suspense_boundary(&dom.runtime(), || {
488                        suspended_nodes.diff_node(&new_suspended_nodes, dom, None::<&mut M>);
489                    });
490
491                    let suspense_context = SuspenseContext::downcast_suspense_boundary_from_scope(
492                        &dom.runtime,
493                        scope_id,
494                    )
495                    .unwrap();
496                    suspense_context.set_suspended_nodes(new_suspended_nodes);
497                }
498                // We have no suspended nodes, and we are not suspended. Just diff the children like normal
499                (None, false) => {
500                    let old_children = last_rendered_node;
501                    let new_children = children;
502
503                    suspense_context.under_suspense_boundary(&dom.runtime(), || {
504                        old_children
505                            .as_vnode()
506                            .diff_node(new_children.as_vnode(), dom, to);
507                    });
508
509                    // Set the last rendered node to the new children
510                    dom.scopes[scope_id.0].last_rendered_node = Some(new_children);
511                }
512                // We have no suspended nodes, but we just became suspended. Move the children to the background
513                (None, true) => {
514                    let old_children = last_rendered_node.as_vnode();
515                    let new_children: VNode = children.into();
516
517                    let new_placeholder = fallback.call(suspense_context.clone());
518
519                    // Move the children to the background
520                    let mount = old_children.mount.get();
521                    let parent = dom.get_mounted_parent(mount);
522
523                    suspense_context.in_suspense_placeholder(&dom.runtime(), || {
524                        old_children.move_node_to_background(
525                            std::slice::from_ref(new_placeholder.as_vnode()),
526                            parent,
527                            dom,
528                            to,
529                        );
530                    });
531
532                    // Then diff the new children in the background
533                    suspense_context.under_suspense_boundary(&dom.runtime(), || {
534                        old_children.diff_node(&new_children, dom, None::<&mut M>);
535                    });
536
537                    // Set the last rendered node to the new suspense placeholder
538                    dom.scopes[scope_id.0].last_rendered_node = Some(new_placeholder);
539
540                    let suspense_context = SuspenseContext::downcast_suspense_boundary_from_scope(
541                        &dom.runtime,
542                        scope_id,
543                    )
544                    .unwrap();
545                    suspense_context.set_suspended_nodes(new_children);
546
547                    un_resolve_suspense(dom, scope_id);
548                }
549                // We have suspended nodes, but we just got out of suspense. Move the suspended nodes to the foreground
550                (Some(_), false) => {
551                    // Take the suspended nodes out of the suspense boundary so the children know that the boundary is not suspended while diffing
552                    let old_suspended_nodes = suspense_context.take_suspended_nodes().unwrap();
553                    let old_placeholder = last_rendered_node;
554                    let new_children = children;
555
556                    // First diff the two children nodes in the background
557                    suspense_context.under_suspense_boundary(&dom.runtime(), || {
558                        old_suspended_nodes.diff_node(new_children.as_vnode(), dom, None::<&mut M>);
559
560                        // Then replace the placeholder with the new children
561                        let mount = old_placeholder.as_vnode().mount.get();
562                        let parent = dom.get_mounted_parent(mount);
563                        old_placeholder.as_vnode().replace(
564                            std::slice::from_ref(new_children.as_vnode()),
565                            parent,
566                            dom,
567                            to,
568                        );
569                    });
570
571                    // Set the last rendered node to the new children
572                    dom.scopes[scope_id.0].last_rendered_node = Some(new_children);
573
574                    mark_suspense_resolved(dom, scope_id);
575                }
576            }
577        })
578    }
579}
580
581/// Move to a resolved suspense state
582fn mark_suspense_resolved(dom: &mut VirtualDom, scope_id: ScopeId) {
583    dom.resolved_scopes.push(scope_id);
584}
585
586/// Move from a resolved suspense state to an suspended state
587fn un_resolve_suspense(dom: &mut VirtualDom, scope_id: ScopeId) {
588    dom.resolved_scopes.retain(|&id| id != scope_id);
589}
590
591impl SuspenseContext {
592    /// Run a closure under a suspense boundary
593    pub fn under_suspense_boundary<O>(&self, runtime: &Runtime, f: impl FnOnce() -> O) -> O {
594        runtime.with_suspense_location(SuspenseLocation::UnderSuspense(self.clone()), f)
595    }
596
597    /// Run a closure under a suspense placeholder
598    pub fn in_suspense_placeholder<O>(&self, runtime: &Runtime, f: impl FnOnce() -> O) -> O {
599        runtime.with_suspense_location(SuspenseLocation::InSuspensePlaceholder(self.clone()), f)
600    }
601
602    /// Try to get a suspense boundary from a scope id
603    pub fn downcast_suspense_boundary_from_scope(
604        runtime: &Runtime,
605        scope_id: ScopeId,
606    ) -> Option<Self> {
607        runtime
608            .get_state(scope_id)
609            .and_then(|scope| scope.suspense_boundary())
610    }
611
612    pub(crate) fn remove_suspended_nodes<M: WriteMutations>(
613        dom: &mut VirtualDom,
614        scope_id: ScopeId,
615        destroy_component_state: bool,
616    ) {
617        let Some(scope) = Self::downcast_suspense_boundary_from_scope(&dom.runtime, scope_id)
618        else {
619            return;
620        };
621        // Remove the suspended nodes
622        if let Some(node) = scope.take_suspended_nodes() {
623            node.remove_node_inner(dom, None::<&mut M>, destroy_component_state, None)
624        }
625    }
626}