Skip to main content

dioxus_core/suspense/
component.rs

1use std::{any::Any, rc::Rc};
2
3use crate::{
4    DynamicNode,
5    diff::context::DiffContext,
6    diff::placement::{InsertionSite, insertion_site_at, splice_streamed_nodes},
7    innerlude::*,
8    mount::{RenderMode, SuspenseBranch},
9    render_driver::{RenderDriver, remove_rendered_output},
10    scope_context::SuspenseLocation,
11};
12
13/// Properties for the [`SuspenseBoundary`] component.
14#[allow(non_camel_case_types)]
15pub struct SuspenseBoundaryProps {
16    fallback: Callback<SuspenseContext, Element>,
17    /// The children of the suspense boundary
18    children: LastRenderedNode,
19}
20
21impl Clone for SuspenseBoundaryProps {
22    fn clone(&self) -> Self {
23        Self {
24            fallback: self.fallback,
25            children: self.children.clone(),
26        }
27    }
28}
29
30#[must_use]
31#[allow(dead_code, non_camel_case_types, non_snake_case)]
32#[doc(hidden)]
33/// Builder for [`SuspenseBoundaryProps`].
34pub struct SuspenseBoundaryPropsBuilder<TypedBuilderFields> {
35    owner: Owner,
36    fields: TypedBuilderFields,
37    _phantom: (),
38}
39
40#[must_use]
41#[allow(dead_code, non_camel_case_types, non_snake_case)]
42#[doc(hidden)]
43/// Component builder for [`SuspenseBoundary`].
44pub struct SuspenseBoundaryComponentBuilder<RenderFn, Marker, TypedBuilderFields> {
45    render_fn: RenderFn,
46    builder: SuspenseBoundaryPropsBuilder<TypedBuilderFields>,
47    _marker: std::marker::PhantomData<fn() -> Marker>,
48}
49
50impl Properties for SuspenseBoundaryProps
51where
52    Self: Clone,
53{
54    type ComponentBuilder<RenderFn, Marker> =
55        SuspenseBoundaryComponentBuilder<RenderFn, Marker, ((), ())>;
56
57    fn component_builder<RenderFn, Marker>(
58        render_fn: RenderFn,
59    ) -> Self::ComponentBuilder<RenderFn, Marker> {
60        SuspenseBoundaryComponentBuilder {
61            render_fn,
62            builder: SuspenseBoundaryPropsBuilder {
63                owner: Owner::default(),
64                fields: ((), ()),
65                _phantom: ::core::default::Default::default(),
66            },
67            _marker: std::marker::PhantomData,
68        }
69    }
70
71    fn memoize(&mut self, new: &Self) -> bool {
72        self.fallback.__point_to(&new.fallback);
73        self.children = new.children.clone();
74        false
75    }
76}
77
78#[allow(dead_code, non_camel_case_types, missing_docs)]
79#[doc(hidden)]
80impl<RenderFn, ComponentMarker, __children>
81    SuspenseBoundaryComponentBuilder<RenderFn, ComponentMarker, ((), __children)>
82{
83    #[allow(clippy::type_complexity)]
84    #[doc(hidden)]
85    pub fn fallback<__Marker>(
86        self,
87        fallback: impl SuperInto<Callback<SuspenseContext, Element>, __Marker>,
88    ) -> SuspenseBoundaryComponentBuilder<
89        RenderFn,
90        ComponentMarker,
91        ((Callback<SuspenseContext, Element>,), __children),
92    > {
93        SuspenseBoundaryComponentBuilder {
94            render_fn: self.render_fn,
95            builder: self.builder.fallback(fallback),
96            _marker: self._marker,
97        }
98    }
99}
100
101#[allow(dead_code, non_camel_case_types, missing_docs)]
102#[doc(hidden)]
103impl<RenderFn, ComponentMarker, __fallback>
104    SuspenseBoundaryComponentBuilder<RenderFn, ComponentMarker, (__fallback, ())>
105{
106    #[allow(clippy::type_complexity)]
107    #[doc(hidden)]
108    pub fn children(
109        self,
110        children: Element,
111    ) -> SuspenseBoundaryComponentBuilder<RenderFn, ComponentMarker, (__fallback, (Element,))> {
112        SuspenseBoundaryComponentBuilder {
113            render_fn: self.render_fn,
114            builder: self.builder.children(children),
115            _marker: self._marker,
116        }
117    }
118}
119#[allow(dead_code, non_camel_case_types, non_snake_case)]
120#[doc(hidden)]
121/// Helper trait used by the generated suspense boundary props builder.
122pub trait SuspenseBoundaryPropsBuilder_Optional<T> {
123    /// Convert the optional builder field into a value.
124    fn into_value<F: FnOnce() -> T>(self, default: F) -> T;
125}
126impl<T> SuspenseBoundaryPropsBuilder_Optional<T> for () {
127    fn into_value<F: FnOnce() -> T>(self, default: F) -> T {
128        default()
129    }
130}
131impl<T> SuspenseBoundaryPropsBuilder_Optional<T> for (T,) {
132    fn into_value<F: FnOnce() -> T>(self, _: F) -> T {
133        self.0
134    }
135}
136#[allow(dead_code, non_camel_case_types, missing_docs)]
137#[doc(hidden)]
138impl<__children> SuspenseBoundaryPropsBuilder<((), __children)> {
139    #[allow(clippy::type_complexity)]
140    #[doc(hidden)]
141    pub fn fallback<__Marker>(
142        self,
143        fallback: impl SuperInto<Callback<SuspenseContext, Element>, __Marker>,
144    ) -> SuspenseBoundaryPropsBuilder<((Callback<SuspenseContext, Element>,), __children)> {
145        let fallback = (with_owner(self.owner.clone(), move || {
146            SuperInto::super_into(fallback)
147        }),);
148        let (_, children) = self.fields;
149        SuspenseBoundaryPropsBuilder {
150            owner: self.owner,
151            fields: (fallback, children),
152            _phantom: self._phantom,
153        }
154    }
155}
156#[allow(dead_code, non_camel_case_types, non_snake_case)]
157#[doc(hidden)]
158/// Error marker for setting the `fallback` field more than once.
159pub enum SuspenseBoundaryPropsBuilder_Error_Repeated_field_fallback {}
160#[allow(dead_code, non_camel_case_types, missing_docs)]
161#[doc(hidden)]
162impl<__children> SuspenseBoundaryPropsBuilder<((Callback<SuspenseContext, Element>,), __children)> {
163    #[deprecated(note = "Repeated field fallback")]
164    #[allow(clippy::type_complexity)]
165    #[doc(hidden)]
166    pub fn fallback(
167        self,
168        _: SuspenseBoundaryPropsBuilder_Error_Repeated_field_fallback,
169    ) -> SuspenseBoundaryPropsBuilder<((Callback<SuspenseContext, Element>,), __children)> {
170        self
171    }
172}
173#[allow(dead_code, non_camel_case_types, missing_docs)]
174#[doc(hidden)]
175impl<__fallback> SuspenseBoundaryPropsBuilder<(__fallback, ())> {
176    #[allow(clippy::type_complexity)]
177    #[doc(hidden)]
178    pub fn children(
179        self,
180        children: Element,
181    ) -> SuspenseBoundaryPropsBuilder<(__fallback, (Element,))> {
182        let children = (children,);
183        let (fallback, _) = self.fields;
184        SuspenseBoundaryPropsBuilder {
185            owner: self.owner,
186            fields: (fallback, children),
187            _phantom: self._phantom,
188        }
189    }
190}
191#[allow(dead_code, non_camel_case_types, non_snake_case)]
192#[doc(hidden)]
193/// Error marker for setting the `children` field more than once.
194pub enum SuspenseBoundaryPropsBuilder_Error_Repeated_field_children {}
195#[allow(dead_code, non_camel_case_types, missing_docs)]
196#[doc(hidden)]
197impl<__fallback> SuspenseBoundaryPropsBuilder<(__fallback, (Element,))> {
198    #[deprecated(note = "Repeated field children")]
199    #[allow(clippy::type_complexity)]
200    #[doc(hidden)]
201    pub fn children(
202        self,
203        _: SuspenseBoundaryPropsBuilder_Error_Repeated_field_children,
204    ) -> SuspenseBoundaryPropsBuilder<(__fallback, (Element,))> {
205        self
206    }
207}
208#[allow(dead_code, non_camel_case_types, non_snake_case)]
209#[doc(hidden)]
210/// Error marker for missing the required `fallback` field.
211pub enum SuspenseBoundaryPropsBuilder_Error_Missing_required_field_fallback {}
212#[allow(dead_code, non_camel_case_types, missing_docs, clippy::panic)]
213#[doc(hidden)]
214impl<__children> SuspenseBoundaryPropsBuilder<((), __children)> {
215    #[deprecated(note = "Missing required field fallback")]
216    #[doc(hidden)]
217    pub fn build(
218        self,
219        _: SuspenseBoundaryPropsBuilder_Error_Missing_required_field_fallback,
220    ) -> SuspenseBoundaryProps {
221        panic!()
222    }
223}
224#[allow(dead_code, non_camel_case_types, missing_docs)]
225#[doc(hidden)]
226/// [`SuspenseBoundaryProps`] bundled with the owner that created its callbacks.
227pub struct SuspenseBoundaryPropsWithOwner {
228    inner: SuspenseBoundaryProps,
229    owner: Owner,
230}
231#[automatically_derived]
232#[allow(dead_code, non_camel_case_types, missing_docs)]
233impl ::core::clone::Clone for SuspenseBoundaryPropsWithOwner {
234    #[inline]
235    fn clone(&self) -> SuspenseBoundaryPropsWithOwner {
236        SuspenseBoundaryPropsWithOwner {
237            inner: ::core::clone::Clone::clone(&self.inner),
238            owner: ::core::clone::Clone::clone(&self.owner),
239        }
240    }
241}
242impl PartialEq for SuspenseBoundaryPropsWithOwner {
243    fn eq(&self, other: &Self) -> bool {
244        self.inner.eq(&other.inner)
245    }
246}
247impl SuspenseBoundaryPropsWithOwner {
248    /// Create a component from the props.
249    pub fn into_vcomponent<M: 'static>(
250        self,
251        render_fn: impl ComponentFunction<SuspenseBoundaryProps, M>,
252    ) -> VComponent {
253        let component_name = std::any::type_name_of_val(&render_fn);
254        let render_fn_ptr = render_fn.fn_ptr();
255        let props = Box::new(VProps::new(
256            move |wrapper: Self| render_fn.rebuild(wrapper.inner),
257            <Self as Properties>::memoize,
258            self,
259            component_name,
260        ));
261        VComponent::new_with_driver(
262            component_name,
263            render_fn_ptr,
264            Rc::new(SuspenseDriver {
265                context: SuspenseContext::new(),
266            }),
267            props,
268        )
269    }
270}
271
272impl<RenderFn, Marker> ComponentBuilderRender<RenderFn, Marker> for SuspenseBoundaryPropsWithOwner
273where
274    RenderFn: ComponentFunction<SuspenseBoundaryProps, Marker>,
275    Marker: 'static,
276{
277    fn into_vcomponent(self, render_fn: RenderFn) -> VComponent {
278        SuspenseBoundaryPropsWithOwner::into_vcomponent(self, render_fn)
279    }
280}
281
282impl Properties for SuspenseBoundaryPropsWithOwner {
283    type ComponentBuilder<RenderFn, Marker> = ();
284
285    fn component_builder<RenderFn, Marker>(
286        _: RenderFn,
287    ) -> Self::ComponentBuilder<RenderFn, Marker> {
288        unreachable!()
289    }
290
291    fn memoize(&mut self, new: &Self) -> bool {
292        self.inner.memoize(&new.inner)
293    }
294}
295#[allow(dead_code, non_camel_case_types, missing_docs)]
296#[doc(hidden)]
297impl<__children: SuspenseBoundaryPropsBuilder_Optional<Element>>
298    SuspenseBoundaryPropsBuilder<((Callback<SuspenseContext, Element>,), __children)>
299{
300    #[doc(hidden)]
301    pub fn build(self) -> SuspenseBoundaryPropsWithOwner {
302        let (fallback, children) = self.fields;
303        let fallback = fallback.0;
304        let children = SuspenseBoundaryPropsBuilder_Optional::into_value(children, VNode::empty);
305        SuspenseBoundaryPropsWithOwner {
306            inner: SuspenseBoundaryProps {
307                fallback,
308                children: LastRenderedNode::new(children),
309            },
310            owner: self.owner,
311        }
312    }
313}
314
315#[allow(dead_code, non_camel_case_types, missing_docs)]
316#[doc(hidden)]
317impl<RenderFn, ComponentMarker, __children: SuspenseBoundaryPropsBuilder_Optional<Element>>
318    SuspenseBoundaryComponentBuilder<
319        RenderFn,
320        ComponentMarker,
321        ((Callback<SuspenseContext, Element>,), __children),
322    >
323{
324    #[doc(hidden)]
325    pub fn build(
326        self,
327    ) -> ComponentBuilderOutput<RenderFn, SuspenseBoundaryPropsWithOwner, ComponentMarker> {
328        ComponentBuilderOutput::new(self.render_fn, self.builder.build())
329    }
330}
331
332#[automatically_derived]
333#[allow(non_camel_case_types)]
334impl ::core::cmp::PartialEq for SuspenseBoundaryProps {
335    #[inline]
336    fn eq(&self, other: &SuspenseBoundaryProps) -> bool {
337        self.fallback == other.fallback && self.children == other.children
338    }
339}
340
341/// Suspense Boundaries let you render a fallback UI while a child component is suspended.
342///
343/// # Example
344///
345/// ```rust
346/// # use dioxus::prelude::*;
347/// # fn Article() -> Element { rsx! { "Article" } }
348/// fn App() -> Element {
349///     rsx! {
350///         SuspenseBoundary {
351///             fallback: |_| rsx! { "Loading..." },
352///             Article {}
353///         }
354///     }
355/// }
356/// ```
357#[allow(non_snake_case)]
358#[cfg_attr(coverage_nightly, coverage(off))]
359pub fn SuspenseBoundary(_: SuspenseBoundaryProps) -> Element {
360    unreachable!("SuspenseBoundary should not be called directly")
361}
362
363/// The rendering lifecycle of a suspense boundary scope.
364struct SuspenseDriver {
365    context: SuspenseContext,
366}
367
368impl RenderDriver for SuspenseDriver {
369    fn as_any(&self) -> &dyn Any {
370        self
371    }
372
373    fn suspense_location(&self, _: SuspenseLocation) -> SuspenseLocation {
374        SuspenseLocation::SuspenseBoundary(self.context.clone())
375    }
376
377    fn create(
378        &self,
379        dom: &mut VirtualDom,
380        scope_id: ScopeId,
381        parent: Option<MountId>,
382        to: Option<&mut (dyn WriteMutations + '_)>,
383    ) -> usize {
384        if let Some(rendered) = dom.scopes[scope_id.index()]
385            .last_rendered_node
386            .as_ref()
387            .map(|rendered| rendered.node().clone())
388        {
389            dom.mark_clean(scope_id);
390            return dom.create_scope(to, scope_id, rendered, parent);
391        }
392
393        self.context.mount(scope_id);
394
395        let nodes = suspense_create(scope_id, parent, dom, to);
396        dom.mark_clean(scope_id);
397        nodes
398    }
399
400    fn diff(
401        &self,
402        dom: &mut VirtualDom,
403        scope_id: ScopeId,
404        _: Option<DiffContext<'_>>,
405        to: Option<&mut (dyn WriteMutations + '_)>,
406    ) {
407        let render_to = to.filter(|_| dom.scope_should_write_now(scope_id));
408        suspense_diff(scope_id, dom, render_to)
409    }
410
411    fn remove(
412        &self,
413        dom: &mut VirtualDom,
414        scope_id: ScopeId,
415        to: Option<&mut (dyn WriteMutations + '_)>,
416        destroy_component_state: bool,
417    ) {
418        // If this is a suspense boundary, remove the suspended nodes as well.
419        //
420        // When we are only moving a component out of the real DOM for an
421        // ancestor suspense boundary, the nested boundary's suspended nodes
422        // are still its background state. Keep them so the nested boundary
423        // can resume or continue diffing while hidden.
424        if destroy_component_state {
425            SuspenseContext::remove_suspended_nodes(dom, scope_id, destroy_component_state);
426        }
427
428        // The scope's rendered output (children or fallback) is removed the
429        // same way a plain component's output is.
430        remove_rendered_output(dom, scope_id, to, destroy_component_state);
431    }
432}
433
434fn suspense_props(dom: &VirtualDom, scope_id: ScopeId) -> &SuspenseBoundaryPropsWithOwner {
435    dom.scopes[scope_id.index()]
436        .props
437        .props()
438        .downcast_ref::<SuspenseBoundaryPropsWithOwner>()
439        .expect("suspense boundary scope carries SuspenseBoundaryPropsWithOwner")
440}
441
442fn suspense_children(dom: &VirtualDom, scope_id: ScopeId) -> LastRenderedNode {
443    suspense_props(dom, scope_id).inner.children.clone()
444}
445
446fn suspense_fallback(dom: &VirtualDom, scope_id: ScopeId) -> Callback<SuspenseContext, Element> {
447    suspense_props(dom, scope_id).inner.fallback
448}
449
450fn store_suspense_children(dom: &mut VirtualDom, scope_id: ScopeId, children: &LastRenderedNode) {
451    let props = dom.scopes[scope_id.index()]
452        .props
453        .props_mut()
454        .downcast_mut::<SuspenseBoundaryPropsWithOwner>()
455        .expect("suspense boundary scope carries SuspenseBoundaryPropsWithOwner");
456    props.inner.children.clone_from(children);
457}
458#[allow(non_snake_case)]
459#[doc(hidden)]
460mod SuspenseBoundary_completions {
461    #[doc(hidden)]
462    #[allow(non_camel_case_types)]
463    /// This enum is generated to help autocomplete the braces after the component. It does nothing
464    pub enum Component {
465        /// Autocomplete variant for `SuspenseBoundary`.
466        SuspenseBoundary {},
467    }
468}
469#[allow(unused)]
470#[doc(hidden)]
471pub use SuspenseBoundary_completions::Component::SuspenseBoundary;
472use generational_box::Owner;
473
474/// Mount a suspense boundary scope: render the children in the background
475/// first, then mount either the children or the fallback depending on whether
476/// anything suspended.
477fn suspense_create(
478    scope_id: ScopeId,
479    parent: Option<MountId>,
480    dom: &mut VirtualDom,
481    to: Option<&mut (dyn WriteMutations + '_)>,
482) -> usize {
483    dom.runtime.clone().with_scope_on_stack(scope_id, || {
484        let suspense_context = dom.runtime.get_state(scope_id).suspense_boundary().unwrap();
485
486        let children = suspense_children(dom, scope_id);
487
488        // First always render the children in the background. Rendering the children may cause this boundary to suspend
489        let background = suspense_context.under_suspense_boundary(&dom.runtime(), || {
490            children.as_vnode().create_mounted(dom, parent, None)
491        });
492
493        store_suspense_children(dom, scope_id, &children);
494
495        // If there are suspended futures, render the fallback
496
497        if !suspense_context.suspended_futures().is_empty() {
498            let placeholder_context = suspense_context.clone();
499            let (node, nodes_created) =
500                suspense_context.in_suspense_placeholder(&dom.runtime(), || {
501                    let fallback = suspense_fallback(dom, scope_id);
502                    let branch = SuspenseBranch::new(children.as_vnode().clone(), background.mount);
503                    store_suspended_branch(dom, scope_id, &branch);
504                    placeholder_context.set_suspended_branch(branch);
505                    let suspense_placeholder =
506                        LastRenderedNode::new(fallback.call(placeholder_context));
507                    let nodes_created = suspense_placeholder
508                        .as_vnode()
509                        .create_mounted(dom, parent, to);
510                    (suspense_placeholder, nodes_created)
511                });
512
513            dom.scopes[scope_id.index()].last_rendered_node =
514                Some(MountedOutput::new(node, nodes_created.mount));
515            nodes_created.nodes
516        } else {
517            // Otherwise promote the background-rendered children into the real dom,
518            // reusing the background mount and its scopes instead of recreating.
519            let nodes_created = suspense_context.under_suspense_boundary(&dom.runtime(), || {
520                children
521                    .as_vnode()
522                    .recreate_with_mount(dom, background.mount, parent, parent, to)
523            });
524            dom.scopes[scope_id.index()].last_rendered_node =
525                Some(MountedOutput::new(children, nodes_created.mount));
526            suspense_context.take_suspended_branch();
527            mark_suspense_resolved(&suspense_context, dom, scope_id);
528
529            nodes_created.nodes
530        }
531    })
532}
533
534impl SuspenseBoundaryProps {
535    /// Manually rerun the children of this suspense boundary without diffing against the old nodes.
536    ///
537    /// This should only be called by dioxus-web after the suspense boundary has been streamed in from the server.
538    #[doc(hidden)]
539    pub fn resolve_suspense<M: WriteMutations>(
540        scope_id: ScopeId,
541        dom: &mut VirtualDom,
542        to: &mut M,
543        push_replacements: impl FnOnce(&mut M) -> usize,
544    ) {
545        dom.runtime.clone().with_scope_on_stack(scope_id, || {
546            let _runtime = RuntimeGuard::new(dom.runtime());
547            let Some(scope_state) = dom.scopes.get_mut(scope_id.index()) else {
548                return;
549            };
550
551            // Reset the suspense context
552            let suspense_context = scope_state.state().suspense_boundary().unwrap().clone();
553            suspense_context.inner.suspended_tasks.borrow_mut().clear();
554
555            // Get the render parent of the suspense boundary so the resolved
556            // children inherit both the right logical parent (for creation) and
557            // the right render parent. Without the render parent, an event
558            // bubbling out of the resolved subtree panics in
559            // `Runtime::child_slot_path` ("child mount should have a render
560            // parent"), the same way the non-streaming `(None, false)` resolve
561            // path copies it onto the diffed mount below.
562            let currently_rendered = scope_state.last_rendered_node.clone().unwrap();
563            let mount = currently_rendered.root_mount();
564            let render_parent = dom.mounted_render_parent(mount);
565            let parent = render_parent.as_ref().map(|parent| parent.parent());
566
567            // Unmount any children to reset any scopes under this suspense boundary
568            let children = suspense_children(dom, scope_id);
569            // Take the suspended nodes out of the suspense boundary so the children know that the boundary is not suspended while diffing
570            if let Some(branch) = suspense_context.take_suspended_branch() {
571                let mount = branch.root_mount();
572                branch.into_root().remove_node(mount, &mut *dom, None);
573            }
574
575            // Streaming replacements are pushed after the target node so the splice can stay
576            // stack-only. Insert streamed nodes before the fallback when it has a DOM node, then
577            // remove the fallback normally. An empty (or all-suspense) fallback renders to nothing,
578            // so streamed nodes are inserted at the boundary's position instead.
579            let placement = match currently_rendered.mounted_vnode().find_first_element(dom) {
580                Some(id) => InsertionSite::before(id),
581                None => insertion_site_at(currently_rendered.mounted_vnode(), dom, None),
582            };
583            splice_streamed_nodes(to, placement, push_replacements);
584            currently_rendered.as_vnode().remove_node(
585                currently_rendered.root_mount(),
586                &mut *dom,
587                Some(to),
588            );
589
590            // First always render the children in the background. Rendering the children may cause this boundary to suspend
591            let created = suspense_context.under_suspense_boundary(&dom.runtime(), || {
592                let mut no_op = crate::NoOpMutations;
593                children
594                    .as_vnode()
595                    .create_mounted(dom, parent, Some(&mut no_op))
596            });
597
598            // Preserve the boundary's render parent on the resolved subtree so
599            // events can bubble out of it (see the comment where `render_parent`
600            // is captured above).
601            if let Some(render_parent) = render_parent {
602                dom.set_mounted_render_parent(created.mount, render_parent);
603            }
604
605            set_rendered_children(dom, scope_id, children, created.mount);
606
607            // Run any closures that were waiting for the suspense to resolve
608            suspense_context.run_resolved_closures(&dom.runtime);
609        })
610    }
611}
612
613/// Diff a suspense boundary scope against its current children/fallback
614/// props.
615///
616/// `to` is the pre-gated writer: [`SuspenseDriver::diff`] applies the scope
617/// write gate before calling in.
618fn suspense_diff(
619    scope_id: ScopeId,
620    dom: &mut VirtualDom,
621    mut to: Option<&mut (dyn WriteMutations + '_)>,
622) {
623    dom.runtime.clone().with_scope_on_stack(scope_id, || {
624        let scope = &dom.scopes[scope_id.index()];
625        let last_rendered_node = scope.last_rendered_node.clone().unwrap();
626        let children = suspense_children(dom, scope_id);
627        let fallback = suspense_fallback(dom, scope_id);
628
629        let suspense_context = scope.state().suspense_boundary().unwrap().clone();
630        let suspended_branch = suspense_context.suspended_branch();
631        let suspended = !suspense_context.suspended_futures().is_empty();
632        match (suspended_branch, suspended) {
633            // We already have suspended nodes that still need to be suspended
634            // Just diff the normal and suspended nodes
635            (Some(suspended_branch), true) => {
636                let suspended_nodes = suspended_branch.root();
637                let suspended_mount = suspended_branch.root_mount();
638                let new_suspended_nodes: VNode = children.as_vnode().clone();
639
640                // Diff the suspended nodes in the background *first*: re-running the
641                // child may cancel its suspend (e.g. a signal flipped a `mode` flag)
642                // and we want to observe that before committing to a fallback render.
643                let new_suspended_mount =
644                    suspense_context.under_suspense_boundary(&dom.runtime(), || {
645                        MountedVNode::new(&suspended_nodes, suspended_mount).diff_node(
646                            &new_suspended_nodes,
647                            dom,
648                            None,
649                        )
650                    });
651                flush_retained_branch_scopes(dom, scope_id);
652
653                if !suspense_context.suspended_futures().is_empty() {
654                    // Still suspended: diff the placeholder against a fresh fallback.
655                    let (new_placeholder, placeholder_mount) = suspense_context
656                        .in_suspense_placeholder(&dom.runtime(), || {
657                            let new_placeholder =
658                                LastRenderedNode::new(fallback.call(suspense_context.clone()));
659                            let placeholder_mount = last_rendered_node.mounted_vnode().diff_node(
660                                new_placeholder.as_vnode(),
661                                dom,
662                                to,
663                            );
664                            (new_placeholder, placeholder_mount)
665                        });
666                    dom.scopes[scope_id.index()].last_rendered_node =
667                        Some(MountedOutput::new(new_placeholder, placeholder_mount));
668                    let branch = SuspenseBranch::new(new_suspended_nodes, new_suspended_mount);
669                    store_suspended_branch(dom, scope_id, &branch);
670                    suspense_context.set_suspended_branch(branch);
671                } else {
672                    // The background diff resolved the suspension. Promote the
673                    // background-rendered nodes by replacing the fallback placeholder.
674                    suspense_context.take_suspended_branch();
675                    set_suspense_mounts_render_mode(
676                        dom,
677                        &new_suspended_nodes,
678                        new_suspended_mount,
679                        RenderMode::Foreground,
680                        false,
681                    );
682                    suspense_context.under_suspense_boundary(&dom.runtime(), || {
683                        new_suspended_nodes.remove_node_inner(
684                            new_suspended_mount,
685                            dom,
686                            None,
687                            false,
688                        );
689                        let mounted_children = dom
690                            .current_mounted_view(new_suspended_mount)
691                            .expect("suspense child");
692                        replace_placeholder_with(
693                            &last_rendered_node,
694                            MountedVNode::new(&mounted_children, new_suspended_mount),
695                            dom,
696                            to.as_deref_mut(),
697                        );
698                    });
699                    set_rendered_children(
700                        dom,
701                        scope_id,
702                        LastRenderedNode::Real(new_suspended_nodes),
703                        new_suspended_mount,
704                    );
705                    mark_suspense_resolved(&suspense_context, dom, scope_id);
706                }
707            }
708            // We have no suspended nodes, and we are not suspended. Just diff the children like normal
709            (None, false) => {
710                let old_render_parent = dom.mounted_render_parent(last_rendered_node.root_mount());
711                let new_mount = suspense_context.under_suspense_boundary(&dom.runtime(), || {
712                    last_rendered_node
713                        .mounted_vnode()
714                        .diff_node(children.as_vnode(), dom, to)
715                });
716                if let Some(old_render_parent) = old_render_parent {
717                    dom.set_mounted_render_parent(new_mount, old_render_parent);
718                }
719
720                set_rendered_children(dom, scope_id, children, new_mount);
721            }
722            // We have no suspended nodes, but we just became suspended. Move the children to the background
723            (None, true) => {
724                let old_children = last_rendered_node;
725                let new_children: VNode = children.as_vnode().clone();
726
727                let new_placeholder =
728                    LastRenderedNode::new(fallback.call(suspense_context.clone()));
729
730                // Move the children to the background
731                let old_children_mount = old_children.root_mount();
732                let parent = dom
733                    .mounted_render_parent(old_children_mount)
734                    .map(|parent| parent.parent());
735
736                let placeholder_mount =
737                    suspense_context.in_suspense_placeholder(&dom.runtime(), || {
738                        let created = old_children.as_vnode().move_node_to_background(
739                            old_children_mount,
740                            new_placeholder.as_vnode(),
741                            parent,
742                            dom,
743                            to.as_deref_mut(),
744                        );
745                        created.mount
746                    });
747
748                // Then diff the new children in the background
749                set_suspense_mounts_render_mode(
750                    dom,
751                    old_children.as_vnode(),
752                    old_children_mount,
753                    RenderMode::Background,
754                    false,
755                );
756                let new_children_mount =
757                    suspense_context.under_suspense_boundary(&dom.runtime(), || {
758                        old_children
759                            .mounted_vnode()
760                            .diff_node(&new_children, dom, None)
761                    });
762                flush_retained_branch_scopes(dom, scope_id);
763
764                if suspense_context.suspended_futures().is_empty() {
765                    let placeholder_output = MountedOutput::new(new_placeholder, placeholder_mount);
766                    set_suspense_mounts_render_mode(
767                        dom,
768                        &new_children,
769                        new_children_mount,
770                        RenderMode::Foreground,
771                        false,
772                    );
773                    suspense_context.under_suspense_boundary(&dom.runtime(), || {
774                        new_children.remove_node_inner(new_children_mount, dom, None, false);
775                        let mounted_children = dom
776                            .current_mounted_view(new_children_mount)
777                            .expect("suspense child");
778                        replace_placeholder_with(
779                            &placeholder_output,
780                            MountedVNode::new(&mounted_children, new_children_mount),
781                            dom,
782                            to.as_deref_mut(),
783                        );
784                    });
785                    set_rendered_children(
786                        dom,
787                        scope_id,
788                        LastRenderedNode::Real(new_children),
789                        new_children_mount,
790                    );
791                    mark_suspense_resolved(&suspense_context, dom, scope_id);
792                } else {
793                    let branch = SuspenseBranch::new(new_children, new_children_mount);
794                    store_suspended_branch(dom, scope_id, &branch);
795                    // Set the last rendered node to the new suspense placeholder
796                    dom.scopes[scope_id.index()].last_rendered_node =
797                        Some(MountedOutput::new(new_placeholder, placeholder_mount));
798                    suspense_context.set_suspended_branch(branch);
799
800                    un_resolve_suspense(dom, scope_id);
801                }
802            }
803            // We have suspended nodes, but we just got out of suspense. Move the suspended nodes to the foreground
804            (Some(_), false) => {
805                // Take the suspended nodes out of the suspense boundary so the children know that the boundary is not suspended while diffing
806                let old_suspended_branch = suspense_context.take_suspended_branch().unwrap();
807                let old_suspended_mount = old_suspended_branch.root_mount();
808                let old_suspended_nodes = old_suspended_branch.root();
809
810                // First diff the two children nodes in the background
811                let mut new_children_mount = old_suspended_mount;
812                suspense_context.under_suspense_boundary(&dom.runtime(), || {
813                    new_children_mount = MountedVNode::new(
814                        &old_suspended_nodes,
815                        old_suspended_mount,
816                    )
817                    .diff_node(children.as_vnode(), dom, None);
818                    set_suspense_mounts_render_mode(
819                        dom,
820                        children.as_vnode(),
821                        new_children_mount,
822                        RenderMode::Foreground,
823                        true,
824                    );
825                    let mounted_children = dom
826                        .current_mounted_view(new_children_mount)
827                        .expect("suspense child");
828                    replace_placeholder_with(
829                        &last_rendered_node,
830                        MountedVNode::new(&mounted_children, new_children_mount),
831                        dom,
832                        to,
833                    );
834                });
835
836                set_rendered_children(dom, scope_id, children, new_children_mount);
837
838                mark_suspense_resolved(&suspense_context, dom, scope_id);
839            }
840        }
841    })
842}
843
844fn flush_retained_branch_scopes(dom: &mut VirtualDom, scope_id: ScopeId) {
845    while let Some(order) = dom.pop_dirty_descendant_scope(scope_id) {
846        let dirty_scope = order.id;
847        let run_scope = dom
848            .runtime
849            .try_get_state(dirty_scope)
850            .filter(|scope| scope.should_run_during_suspense())
851            .is_some();
852        if run_scope {
853            dom.runtime.clone().while_rendering(|| {
854                dom.run_and_diff_scope(None, dirty_scope);
855            });
856        }
857    }
858}
859
860fn set_rendered_children(
861    dom: &mut VirtualDom,
862    scope_id: ScopeId,
863    children: LastRenderedNode,
864    root_mount: MountId,
865) {
866    store_suspense_children(dom, scope_id, &children);
867    dom.scopes[scope_id.index()].last_rendered_node =
868        Some(MountedOutput::new(children, root_mount));
869    dom.runtime
870        .get_state(scope_id)
871        .set_root_mount(Some(root_mount));
872}
873
874fn store_suspended_branch(dom: &mut VirtualDom, scope_id: ScopeId, branch: &SuspenseBranch) {
875    set_suspense_mounts_render_mode(
876        dom,
877        &branch.root(),
878        branch.root_mount(),
879        RenderMode::Background,
880        false,
881    );
882    store_suspense_children(dom, scope_id, &LastRenderedNode::Real(branch.root()));
883}
884
885fn replace_placeholder_with(
886    placeholder: &MountedOutput,
887    children: MountedVNode<'_>,
888    dom: &mut VirtualDom,
889    to: Option<&mut (dyn WriteMutations + '_)>,
890) {
891    // Invariant: `placeholder` is the currently visible fallback and `children` is the already
892    // materialized retained branch.
893    let placeholder_mount = placeholder.root_mount();
894    let parent = dom
895        .mounted_render_parent(placeholder_mount)
896        .map(|parent| parent.parent());
897    let mut to = to.filter(|_| dom.mount_should_render(placeholder_mount));
898    dom.copy_render_parent_slot(placeholder_mount, children.mount());
899    // Promote the already-materialized retained branch by reusing its mount (and
900    // scope subtree) instead of rebuilding it, so component state and scope ids
901    // survive the fallback -> children swap.
902    if let Some(to) = to.as_deref_mut() {
903        let site = insertion_site_at(placeholder.mounted_vnode(), dom, None);
904        site.create_and_place_with_result(to, dom.runtime.clone(), |to| {
905            let created = children.vnode().recreate_with_mount(
906                dom,
907                children.mount(),
908                parent,
909                parent,
910                Some(to),
911            );
912            (created.nodes, created)
913        });
914    } else {
915        children
916            .vnode()
917            .recreate_with_mount(dom, children.mount(), parent, parent, None);
918    }
919    placeholder
920        .as_vnode()
921        .remove_node(placeholder_mount, dom, to);
922}
923
924/// Move to a resolved suspense state
925fn mark_suspense_resolved(
926    suspense_context: &SuspenseContext,
927    dom: &mut VirtualDom,
928    scope_id: ScopeId,
929) {
930    dom.resolved_scopes.push(scope_id);
931    // Run any closures that were waiting for the suspense to resolve
932    suspense_context.run_resolved_closures(&dom.runtime);
933}
934
935fn set_suspense_mounts_render_mode(
936    dom: &mut VirtualDom,
937    vnode: &VNode,
938    mount: MountId,
939    mode: RenderMode,
940    rerun_dirty_scopes: bool,
941) {
942    // Invariant: the retained suspense branch mode is subtree-wide.
943    dom.set_mount_mode(mount, mode);
944
945    for anchor in vnode.dynamic_anchors() {
946        for slot in anchor.nodes() {
947            let idx = slot.index();
948            match &*slot {
949                DynamicNode::Component(_) => {
950                    let scope_id = dom.unchecked_mounted_dynamic_component_scope(mount, idx);
951                    if rerun_dirty_scopes && dom.mark_clean(scope_id) {
952                        dom.run_and_diff_scope(None, scope_id);
953                    }
954
955                    let rendered = dom.scopes[scope_id.index()]
956                        .last_rendered_node
957                        .clone()
958                        .expect("scope output");
959                    set_suspense_mounts_render_mode(
960                        dom,
961                        rendered.as_vnode(),
962                        rendered.root_mount(),
963                        mode,
964                        rerun_dirty_scopes,
965                    );
966                }
967                DynamicNode::Fragment(nodes) => {
968                    let mounts = dom.mounted_fragment_children_exact(mount, idx, nodes.len());
969                    for (node, mount) in nodes.iter().zip(mounts) {
970                        set_suspense_mounts_render_mode(dom, node, mount, mode, rerun_dirty_scopes);
971                    }
972                }
973                DynamicNode::Text(_) => {}
974            }
975        }
976    }
977}
978
979/// Move from a resolved suspense state to an suspended state
980fn un_resolve_suspense(dom: &mut VirtualDom, scope_id: ScopeId) {
981    dom.resolved_scopes.retain(|&id| id != scope_id);
982}
983
984impl SuspenseContext {
985    /// Run a closure under a suspense boundary
986    pub(crate) fn under_suspense_boundary<O>(&self, runtime: &Runtime, f: impl FnOnce() -> O) -> O {
987        runtime.with_suspense_location(SuspenseLocation::UnderSuspense(self.clone()), f)
988    }
989
990    /// Run a closure under a suspense placeholder
991    pub(crate) fn in_suspense_placeholder<O>(&self, runtime: &Runtime, f: impl FnOnce() -> O) -> O {
992        runtime.with_suspense_location(SuspenseLocation::InSuspensePlaceholder(self.clone()), f)
993    }
994
995    /// Try to get a suspense boundary from a scope id
996    pub fn downcast_suspense_boundary_from_scope(
997        runtime: &Runtime,
998        scope_id: ScopeId,
999    ) -> Option<Self> {
1000        runtime.try_get_state(scope_id)?.suspense_boundary()
1001    }
1002
1003    pub(crate) fn remove_suspended_nodes(
1004        dom: &mut VirtualDom,
1005        scope_id: ScopeId,
1006        destroy_component_state: bool,
1007    ) {
1008        if let Some(scope) = Self::downcast_suspense_boundary_from_scope(&dom.runtime, scope_id)
1009            && let Some(branch) = scope.take_suspended_branch()
1010        {
1011            let mount = branch.root_mount();
1012            branch
1013                .into_root()
1014                .remove_node_inner(mount, dom, None, destroy_component_state)
1015        }
1016    }
1017}