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: LastRenderedNode,
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 {
209                fallback,
210                children: LastRenderedNode::new(children),
211            },
212            owner: self.owner,
213        }
214    }
215}
216#[automatically_derived]
217#[allow(non_camel_case_types)]
218impl ::core::cmp::PartialEq for SuspenseBoundaryProps {
219    #[inline]
220    fn eq(&self, other: &SuspenseBoundaryProps) -> bool {
221        self.fallback == other.fallback && self.children == other.children
222    }
223}
224
225/// Suspense Boundaries let you render a fallback UI while a child component is suspended.
226///
227/// # Example
228///
229/// ```rust
230/// # use dioxus::prelude::*;
231/// # fn Article() -> Element { rsx! { "Article" } }
232/// fn App() -> Element {
233///     rsx! {
234///         SuspenseBoundary {
235///             fallback: |_| rsx! { "Loading..." },
236///             Article {}
237///         }
238///     }
239/// }
240/// ```
241#[allow(non_snake_case)]
242pub fn SuspenseBoundary(mut __props: SuspenseBoundaryProps) -> Element {
243    unreachable!("SuspenseBoundary should not be called directly")
244}
245#[allow(non_snake_case)]
246#[doc(hidden)]
247mod SuspenseBoundary_completions {
248    #[doc(hidden)]
249    #[allow(non_camel_case_types)]
250    /// This enum is generated to help autocomplete the braces after the component. It does nothing
251    pub enum Component {
252        SuspenseBoundary {},
253    }
254}
255use generational_box::Owner;
256#[allow(unused)]
257pub use SuspenseBoundary_completions::Component::SuspenseBoundary;
258
259/// Suspense has a custom diffing algorithm that diffs the suspended nodes in the background without rendering them
260impl SuspenseBoundaryProps {
261    /// Try to downcast [`AnyProps`] to [`SuspenseBoundaryProps`]
262    pub(crate) fn downcast_from_props(props: &mut dyn AnyProps) -> Option<&mut Self> {
263        let inner: Option<&mut SuspenseBoundaryPropsWithOwner> = props.props_mut().downcast_mut();
264        inner.map(|inner| &mut inner.inner)
265    }
266
267    pub(crate) fn create<M: WriteMutations>(
268        mount: MountId,
269        idx: usize,
270        component: &VComponent,
271        parent: Option<ElementRef>,
272        dom: &mut VirtualDom,
273        to: Option<&mut M>,
274    ) -> usize {
275        let mut scope_id = ScopeId(dom.get_mounted_dyn_node(mount, idx));
276        // 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
277        if scope_id.is_placeholder() {
278            {
279                let suspense_context = SuspenseContext::new();
280
281                let suspense_boundary_location =
282                    crate::scope_context::SuspenseLocation::SuspenseBoundary(
283                        suspense_context.clone(),
284                    );
285                dom.runtime
286                    .clone()
287                    .with_suspense_location(suspense_boundary_location, || {
288                        let scope_state = dom
289                            .new_scope(component.props.duplicate(), component.name)
290                            .state();
291                        suspense_context.mount(scope_state.id);
292                        scope_id = scope_state.id;
293                    });
294            }
295
296            // Store the scope id for the next render
297            dom.set_mounted_dyn_node(mount, idx, scope_id.0);
298        }
299        dom.runtime.clone().with_scope_on_stack(scope_id, || {
300            let scope_state = &mut dom.scopes[scope_id.0];
301            let props = Self::downcast_from_props(&mut *scope_state.props).unwrap();
302            let suspense_context =
303                SuspenseContext::downcast_suspense_boundary_from_scope(&dom.runtime, scope_id)
304                    .unwrap();
305
306            let children = props.children.clone();
307
308            // First always render the children in the background. Rendering the children may cause this boundary to suspend
309            suspense_context.under_suspense_boundary(&dom.runtime(), || {
310                children.create(dom, parent, None::<&mut M>);
311            });
312
313            // Store the (now mounted) children back into the scope state
314            let scope_state = &mut dom.scopes[scope_id.0];
315            let props = Self::downcast_from_props(&mut *scope_state.props).unwrap();
316            props.children.clone_from(&children);
317
318            let scope_state = &mut dom.scopes[scope_id.0];
319            let suspense_context = scope_state
320                .state()
321                .suspense_location()
322                .suspense_context()
323                .unwrap()
324                .clone();
325
326            // If there are suspended futures, render the fallback
327            let nodes_created = if !suspense_context.suspended_futures().is_empty() {
328                let (node, nodes_created) =
329                    suspense_context.in_suspense_placeholder(&dom.runtime(), || {
330                        let scope_state = &mut dom.scopes[scope_id.0];
331                        let props = Self::downcast_from_props(&mut *scope_state.props).unwrap();
332                        let suspense_context =
333                            SuspenseContext::downcast_suspense_boundary_from_scope(
334                                &dom.runtime,
335                                scope_id,
336                            )
337                            .unwrap();
338                        suspense_context.set_suspended_nodes(children.as_vnode().clone());
339                        let suspense_placeholder =
340                            LastRenderedNode::new(props.fallback.call(suspense_context));
341                        let nodes_created = suspense_placeholder.create(dom, parent, to);
342                        (suspense_placeholder, nodes_created)
343                    });
344
345                let scope_state = &mut dom.scopes[scope_id.0];
346                scope_state.last_rendered_node = Some(node);
347
348                nodes_created
349            } else {
350                // Otherwise just render the children in the real dom
351                debug_assert!(children.mount.get().mounted());
352                let nodes_created = suspense_context
353                    .under_suspense_boundary(&dom.runtime(), || children.create(dom, parent, to));
354                let scope_state = &mut dom.scopes[scope_id.0];
355                scope_state.last_rendered_node = children.into();
356                let suspense_context =
357                    SuspenseContext::downcast_suspense_boundary_from_scope(&dom.runtime, scope_id)
358                        .unwrap();
359                suspense_context.take_suspended_nodes();
360                mark_suspense_resolved(&suspense_context, dom, scope_id);
361
362                nodes_created
363            };
364            nodes_created
365        })
366    }
367
368    #[doc(hidden)]
369    /// Manually rerun the children of this suspense boundary without diffing against the old nodes.
370    ///
371    /// This should only be called by dioxus-web after the suspense boundary has been streamed in from the server.
372    pub fn resolve_suspense<M: WriteMutations>(
373        scope_id: ScopeId,
374        dom: &mut VirtualDom,
375        to: &mut M,
376        only_write_templates: impl FnOnce(&mut M),
377        replace_with: usize,
378    ) {
379        dom.runtime.clone().with_scope_on_stack(scope_id, || {
380            let _runtime = RuntimeGuard::new(dom.runtime());
381            let Some(scope_state) = dom.scopes.get_mut(scope_id.0) else {
382                return;
383            };
384
385            // Reset the suspense context
386            let suspense_context = scope_state
387                .state()
388                .suspense_location()
389                .suspense_context()
390                .unwrap()
391                .clone();
392            suspense_context.inner.suspended_tasks.borrow_mut().clear();
393
394            // Get the parent of the suspense boundary to later create children with the right parent
395            let currently_rendered = scope_state.last_rendered_node.clone().unwrap();
396            let mount = currently_rendered.mount.get();
397            let parent = {
398                let mounts = dom.runtime.mounts.borrow();
399                mounts
400                    .get(mount.0)
401                    .expect("suspense placeholder is not mounted")
402                    .parent
403            };
404
405            let props = Self::downcast_from_props(&mut *scope_state.props).unwrap();
406
407            // Unmount any children to reset any scopes under this suspense boundary
408            let children = props.children.clone();
409            let suspense_context =
410                SuspenseContext::downcast_suspense_boundary_from_scope(&dom.runtime, scope_id)
411                    .unwrap();
412
413            // Take the suspended nodes out of the suspense boundary so the children know that the boundary is not suspended while diffing
414            let suspended = suspense_context.take_suspended_nodes();
415            if let Some(node) = suspended {
416                node.remove_node(&mut *dom, None::<&mut M>, None);
417            }
418
419            // Replace the rendered nodes with resolved nodes
420            currently_rendered.remove_node(&mut *dom, Some(to), Some(replace_with));
421
422            // Switch to only writing templates
423            only_write_templates(to);
424
425            children.mount.take();
426
427            // First always render the children in the background. Rendering the children may cause this boundary to suspend
428            suspense_context.under_suspense_boundary(&dom.runtime(), || {
429                children.create(dom, parent, Some(to));
430            });
431
432            // Store the (now mounted) children back into the scope state
433            let scope_state = &mut dom.scopes[scope_id.0];
434            let props = Self::downcast_from_props(&mut *scope_state.props).unwrap();
435            props.children.clone_from(&children);
436            scope_state.last_rendered_node = Some(children);
437
438            // Run any closures that were waiting for the suspense to resolve
439            suspense_context.run_resolved_closures(&dom.runtime);
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.clone().unwrap();
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.as_vnode().clone();
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 =
474                                LastRenderedNode::new(fallback.call(suspense_context.clone()));
475
476                            old_placeholder.diff_node(&new_placeholder, dom, to);
477                            new_placeholder
478                        });
479
480                    // Set the last rendered node to the placeholder
481                    dom.scopes[scope_id.0].last_rendered_node = Some(new_placeholder);
482
483                    // Diff the suspended nodes in the background
484                    suspense_context.under_suspense_boundary(&dom.runtime(), || {
485                        suspended_nodes.diff_node(&new_suspended_nodes, dom, None::<&mut M>);
486                    });
487
488                    let suspense_context = SuspenseContext::downcast_suspense_boundary_from_scope(
489                        &dom.runtime,
490                        scope_id,
491                    )
492                    .unwrap();
493                    suspense_context.set_suspended_nodes(new_suspended_nodes);
494                }
495                // We have no suspended nodes, and we are not suspended. Just diff the children like normal
496                (None, false) => {
497                    let old_children = last_rendered_node;
498                    let new_children = children;
499
500                    suspense_context.under_suspense_boundary(&dom.runtime(), || {
501                        old_children.diff_node(&new_children, dom, to);
502                    });
503
504                    // Set the last rendered node to the new children
505                    dom.scopes[scope_id.0].last_rendered_node = new_children.into();
506                }
507                // We have no suspended nodes, but we just became suspended. Move the children to the background
508                (None, true) => {
509                    let old_children = last_rendered_node;
510                    let new_children: VNode = children.as_vnode().clone();
511
512                    let new_placeholder =
513                        LastRenderedNode::new(fallback.call(suspense_context.clone()));
514
515                    // Move the children to the background
516                    let mount = old_children.mount.get();
517                    let parent = dom.get_mounted_parent(mount);
518
519                    suspense_context.in_suspense_placeholder(&dom.runtime(), || {
520                        old_children.move_node_to_background(
521                            std::slice::from_ref(&new_placeholder),
522                            parent,
523                            dom,
524                            to,
525                        );
526                    });
527
528                    // Then diff the new children in the background
529                    suspense_context.under_suspense_boundary(&dom.runtime(), || {
530                        old_children.diff_node(&new_children, dom, None::<&mut M>);
531                    });
532
533                    // Set the last rendered node to the new suspense placeholder
534                    dom.scopes[scope_id.0].last_rendered_node = Some(new_placeholder);
535
536                    let suspense_context = SuspenseContext::downcast_suspense_boundary_from_scope(
537                        &dom.runtime,
538                        scope_id,
539                    )
540                    .unwrap();
541                    suspense_context.set_suspended_nodes(new_children);
542
543                    un_resolve_suspense(dom, scope_id);
544                }
545                // We have suspended nodes, but we just got out of suspense. Move the suspended nodes to the foreground
546                (Some(_), false) => {
547                    // Take the suspended nodes out of the suspense boundary so the children know that the boundary is not suspended while diffing
548                    let old_suspended_nodes = suspense_context.take_suspended_nodes().unwrap();
549                    let old_placeholder = last_rendered_node;
550                    let new_children = children;
551
552                    // First diff the two children nodes in the background
553                    suspense_context.under_suspense_boundary(&dom.runtime(), || {
554                        old_suspended_nodes.diff_node(&new_children, dom, None::<&mut M>);
555
556                        // Then replace the placeholder with the new children
557                        let mount = old_placeholder.mount.get();
558                        let parent = dom.get_mounted_parent(mount);
559                        old_placeholder.replace(
560                            std::slice::from_ref(&new_children),
561                            parent,
562                            dom,
563                            to,
564                        );
565                    });
566
567                    // Set the last rendered node to the new children
568                    dom.scopes[scope_id.0].last_rendered_node = Some(new_children);
569
570                    mark_suspense_resolved(&suspense_context, dom, scope_id);
571                }
572            }
573        })
574    }
575}
576
577/// Move to a resolved suspense state
578fn mark_suspense_resolved(
579    suspense_context: &SuspenseContext,
580    dom: &mut VirtualDom,
581    scope_id: ScopeId,
582) {
583    dom.resolved_scopes.push(scope_id);
584    // Run any closures that were waiting for the suspense to resolve
585    suspense_context.run_resolved_closures(&dom.runtime);
586}
587
588/// Move from a resolved suspense state to an suspended state
589fn un_resolve_suspense(dom: &mut VirtualDom, scope_id: ScopeId) {
590    dom.resolved_scopes.retain(|&id| id != scope_id);
591}
592
593impl SuspenseContext {
594    /// Run a closure under a suspense boundary
595    pub(crate) fn under_suspense_boundary<O>(&self, runtime: &Runtime, f: impl FnOnce() -> O) -> O {
596        runtime.with_suspense_location(SuspenseLocation::UnderSuspense(self.clone()), f)
597    }
598
599    /// Run a closure under a suspense placeholder
600    pub(crate) fn in_suspense_placeholder<O>(&self, runtime: &Runtime, f: impl FnOnce() -> O) -> O {
601        runtime.with_suspense_location(SuspenseLocation::InSuspensePlaceholder(self.clone()), f)
602    }
603
604    /// Try to get a suspense boundary from a scope id
605    pub fn downcast_suspense_boundary_from_scope(
606        runtime: &Runtime,
607        scope_id: ScopeId,
608    ) -> Option<Self> {
609        runtime
610            .try_get_state(scope_id)
611            .and_then(|scope| scope.suspense_boundary())
612    }
613
614    pub(crate) fn remove_suspended_nodes<M: WriteMutations>(
615        dom: &mut VirtualDom,
616        scope_id: ScopeId,
617        destroy_component_state: bool,
618    ) {
619        let Some(scope) = Self::downcast_suspense_boundary_from_scope(&dom.runtime, scope_id)
620        else {
621            return;
622        };
623        // Remove the suspended nodes
624        if let Some(node) = scope.take_suspended_nodes() {
625            node.remove_node_inner(dom, None::<&mut M>, destroy_component_state, None)
626        }
627    }
628}