Skip to main content
This is unreleased documentation for Yew Next version.
For up-to-date documentation, see the latest version on docs.rs.

yew/suspense/
component.rs

1use crate::html::{Html, Properties};
2
3/// Properties for [Suspense].
4#[derive(Properties, PartialEq, Debug, Clone)]
5pub struct SuspenseProps {
6    /// The Children of the current Suspense Component.
7    #[prop_or_default]
8    pub children: Html,
9
10    /// The Fallback UI of the current Suspense Component.
11    #[prop_or_default]
12    pub fallback: Html,
13}
14
15#[cfg(any(feature = "csr", feature = "ssr"))]
16mod feat_csr_ssr {
17    #[cfg(feature = "csr")]
18    use std::cell::RefCell;
19
20    use super::*;
21    #[cfg(feature = "csr")]
22    use crate::html::PendingRendered;
23    use crate::html::{Component, Context, Html, Scope};
24    use crate::suspense::Suspension;
25    #[cfg(feature = "hydration")]
26    use crate::suspense::SuspensionHandle;
27    use crate::virtual_dom::{VNode, VSuspense};
28    use crate::{component, html};
29
30    #[derive(Properties, PartialEq, Debug, Clone)]
31    pub(crate) struct BaseSuspenseProps {
32        pub children: Html,
33        #[prop_or(None)]
34        pub fallback: Option<Html>,
35    }
36
37    #[derive(Debug)]
38    pub(crate) enum BaseSuspenseMsg {
39        Suspend(Suspension),
40        Resume(Suspension),
41    }
42
43    pub(crate) struct BaseSuspense {
44        suspensions: Vec<Suspension>,
45        #[cfg(feature = "hydration")]
46        hydration_handle: Option<SuspensionHandle>,
47        /// Rendered runners for child components that resumed while this
48        /// Suspense was still suspended (because of other pending siblings).
49        /// Drained in `rendered` once the Suspense fully un-suspends, so
50        /// effects fire only after children's DOM has been shifted into the
51        /// live tree.
52        ///
53        /// A small `Vec` is used over a map; the expected population is the
54        /// number of suspending direct descendants resumed in one boundary
55        /// transition, typically just a handful.
56        #[cfg(feature = "csr")]
57        pending_rendered: RefCell<Vec<(usize, PendingRendered)>>,
58    }
59
60    impl std::fmt::Debug for BaseSuspense {
61        fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
62            f.debug_struct("BaseSuspense")
63                .field("suspensions", &self.suspensions)
64                .finish()
65        }
66    }
67
68    impl Component for BaseSuspense {
69        type Message = BaseSuspenseMsg;
70        type Properties = BaseSuspenseProps;
71
72        fn create(_ctx: &Context<Self>) -> Self {
73            #[cfg(not(feature = "hydration"))]
74            let suspensions = Vec::new();
75
76            // We create a suspension to block suspense until its rendered method is notified.
77            #[cfg(feature = "hydration")]
78            let (suspensions, hydration_handle) = {
79                use crate::callback::Callback;
80                use crate::html::RenderMode;
81
82                match _ctx.creation_mode() {
83                    RenderMode::Hydration => {
84                        let link = _ctx.link().clone();
85                        let (s, handle) = Suspension::new();
86                        s.listen(Callback::from(move |s| {
87                            link.send_message(BaseSuspenseMsg::Resume(s));
88                        }));
89                        (vec![s], Some(handle))
90                    }
91                    _ => (Vec::new(), None),
92                }
93            };
94
95            Self {
96                suspensions,
97                #[cfg(feature = "hydration")]
98                hydration_handle,
99                #[cfg(feature = "csr")]
100                pending_rendered: RefCell::new(Vec::new()),
101            }
102        }
103
104        fn update(&mut self, ctx: &Context<Self>, msg: Self::Message) -> bool {
105            match msg {
106                Self::Message::Suspend(m) => {
107                    assert!(
108                        ctx.props().fallback.is_some(),
109                        "You cannot suspend from a component rendered as a fallback."
110                    );
111
112                    if m.resumed() {
113                        return false;
114                    }
115
116                    // If a suspension already exists, ignore it.
117                    if self.suspensions.iter().any(|n| n == &m) {
118                        return false;
119                    }
120
121                    self.suspensions.push(m);
122
123                    true
124                }
125                Self::Message::Resume(ref m) => {
126                    let suspensions_len = self.suspensions.len();
127                    self.suspensions.retain(|n| m != n);
128
129                    suspensions_len != self.suspensions.len()
130                }
131            }
132        }
133
134        fn view(&self, ctx: &Context<Self>) -> Html {
135            let BaseSuspenseProps { children, fallback } = (*ctx.props()).clone();
136            let children = VNode::VList(::std::rc::Rc::new(
137                crate::virtual_dom::VList::with_children(vec![children], None),
138            ));
139
140            match fallback {
141                Some(fallback) => {
142                    let vsuspense = VSuspense::new(
143                        children,
144                        fallback,
145                        !self.suspensions.is_empty(),
146                        // We don't need to key this as the key will be applied to the component.
147                        None,
148                    );
149
150                    VNode::from(vsuspense)
151                }
152                None => children,
153            }
154        }
155
156        fn rendered(&mut self, _ctx: &Context<Self>, first_render: bool) {
157            #[cfg(not(feature = "hydration"))]
158            let _ = first_render;
159            #[cfg(feature = "hydration")]
160            if first_render {
161                if let Some(m) = self.hydration_handle.take() {
162                    m.resume();
163                }
164            }
165            // Fire deferred rendered callbacks for children that resumed while
166            // we were still suspended. Only safe now that we're un-suspended:
167            // the last reconcile shifted their DOM into the live tree.
168            #[cfg(feature = "csr")]
169            if self.suspensions.is_empty() {
170                let pending = std::mem::take(&mut *self.pending_rendered.borrow_mut());
171                for (comp_id, p) in pending {
172                    p.schedule(comp_id);
173                }
174            }
175        }
176    }
177
178    impl BaseSuspense {
179        pub(crate) fn suspend(scope: &Scope<Self>, s: Suspension) {
180            scope.send_message(BaseSuspenseMsg::Suspend(s));
181        }
182
183        pub(crate) fn resume(scope: &Scope<Self>, s: Suspension) {
184            scope.send_message(BaseSuspenseMsg::Resume(s));
185        }
186
187        /// Queue a child component's `rendered` lifecycle to be scheduled once
188        /// this Suspense fully un-suspends and its reconcile has shifted the
189        /// child's DOM into the live tree. If the child already has a pending
190        /// entry (e.g. it re-committed between suspensions), the two are
191        /// merged so `first_render=true` is not lost.
192        #[cfg(feature = "csr")]
193        pub(crate) fn defer_rendered(
194            scope: &Scope<Self>,
195            comp_id: usize,
196            pending: PendingRendered,
197        ) {
198            let Some(comp) = scope.get_component() else {
199                return;
200            };
201            let mut q = comp.pending_rendered.borrow_mut();
202            if let Some(slot) = q.iter_mut().find(|(id, _)| *id == comp_id) {
203                slot.1.absorb(pending);
204            } else {
205                q.push((comp_id, pending));
206            }
207        }
208    }
209
210    /// Suspend rendering and show a fallback UI until the underlying task completes.
211    #[component]
212    pub fn Suspense(props: &SuspenseProps) -> Html {
213        let SuspenseProps { children, fallback } = props.clone();
214
215        let fallback = html! {
216            <BaseSuspense>
217                {fallback}
218            </BaseSuspense>
219        };
220
221        html! {
222            <BaseSuspense {fallback}>
223                {children}
224            </BaseSuspense>
225        }
226    }
227}
228
229#[cfg(any(feature = "csr", feature = "ssr"))]
230pub use feat_csr_ssr::*;
231
232#[cfg(not(any(feature = "ssr", feature = "csr")))]
233mod feat_no_csr_ssr {
234    use super::*;
235    use crate::component;
236
237    /// Suspend rendering and show a fallback UI until the underlying task completes.
238    #[component]
239    pub fn Suspense(_props: &SuspenseProps) -> Html {
240        Html::default()
241    }
242}
243
244#[cfg(not(any(feature = "ssr", feature = "csr")))]
245pub use feat_no_csr_ssr::*;