dioxus_core/suspense/
mod.rs

1//! Suspense allows you to render a placeholder while nodes are waiting for data in the background
2//!
3//! During suspense on the server:
4//! - Rebuild once
5//! - Send page with loading placeholders down to the client
6//! - loop
7//!   - Poll (only) suspended futures
8//!   - If a scope is marked as dirty and that scope is a suspense boundary, under a suspended boundary, or the suspense placeholder, rerun the scope
9//!     - If it is a different scope, ignore it and warn the user
10//!   - Rerender the scope on the server and send down the nodes under a hidden div with serialized data
11//!
12//! During suspense on the web:
13//! - Rebuild once without running server futures
14//! - Rehydrate the placeholders that were initially sent down. At this point, no suspense nodes are resolved so the client and server pages should be the same
15//! - loop
16//!   - Wait for work or suspense data
17//!   - If suspense data comes in
18//!     - replace the suspense placeholder
19//!     - get any data associated with the suspense placeholder and rebuild nodes under the suspense that was resolved
20//!     - rehydrate the suspense placeholders that were at that node
21//!   - If work comes in
22//!     - Just do the work; this may remove suspense placeholders that the server hasn't yet resolved. If we see new data come in from the server about that node, ignore it
23//!
24//! Generally suspense placeholders should not be stateful because they are driven from the server. If they are stateful and the client renders something different, hydration will fail.
25
26mod component;
27pub use component::*;
28
29use crate::innerlude::*;
30use std::{
31    cell::{Cell, Ref, RefCell},
32    fmt::Debug,
33    rc::Rc,
34};
35
36/// A task that has been suspended which may have an optional loading placeholder
37#[derive(Clone, PartialEq, Debug)]
38pub struct SuspendedFuture {
39    origin: ScopeId,
40    task: Task,
41    pub(crate) placeholder: VNode,
42}
43
44impl SuspendedFuture {
45    /// Create a new suspended future
46    pub fn new(task: Task) -> Self {
47        Self {
48            task,
49            origin: current_scope_id().unwrap_or_else(|e| panic!("{}", e)),
50            placeholder: VNode::placeholder(),
51        }
52    }
53
54    /// Get a placeholder to display while the future is suspended
55    pub fn suspense_placeholder(&self) -> Option<VNode> {
56        if self.placeholder == VNode::placeholder() {
57            None
58        } else {
59            Some(self.placeholder.clone())
60        }
61    }
62
63    /// Set a new placeholder the SuspenseBoundary may use to display while the future is suspended
64    pub fn with_placeholder(mut self, placeholder: VNode) -> Self {
65        self.placeholder = placeholder;
66        self
67    }
68
69    /// Get the task that was suspended
70    pub fn task(&self) -> Task {
71        self.task
72    }
73
74    /// Create a deep clone of this suspended future
75    pub(crate) fn deep_clone(&self) -> Self {
76        Self {
77            task: self.task,
78            placeholder: self.placeholder.deep_clone(),
79            origin: self.origin,
80        }
81    }
82}
83
84/// A context with information about suspended components
85#[derive(Debug, Clone)]
86pub struct SuspenseContext {
87    inner: Rc<SuspenseBoundaryInner>,
88}
89
90impl PartialEq for SuspenseContext {
91    fn eq(&self, other: &Self) -> bool {
92        Rc::ptr_eq(&self.inner, &other.inner)
93    }
94}
95
96impl SuspenseContext {
97    /// Create a new suspense boundary in a specific scope
98    pub(crate) fn new() -> Self {
99        Self {
100            inner: Rc::new(SuspenseBoundaryInner {
101                suspended_tasks: RefCell::new(vec![]),
102                id: Cell::new(ScopeId::ROOT),
103                suspended_nodes: Default::default(),
104                frozen: Default::default(),
105                after_suspense_resolved: Default::default(),
106            }),
107        }
108    }
109
110    /// Mount the context in a specific scope
111    pub(crate) fn mount(&self, scope: ScopeId) {
112        self.inner.id.set(scope);
113    }
114
115    /// Get the suspense boundary's suspended nodes
116    pub fn suspended_nodes(&self) -> Option<VNode> {
117        self.inner
118            .suspended_nodes
119            .borrow()
120            .as_ref()
121            .map(|node| node.clone())
122    }
123
124    /// Set the suspense boundary's suspended nodes
125    pub(crate) fn set_suspended_nodes(&self, suspended_nodes: VNode) {
126        self.inner
127            .suspended_nodes
128            .borrow_mut()
129            .replace(suspended_nodes);
130    }
131
132    /// Take the suspense boundary's suspended nodes
133    pub(crate) fn take_suspended_nodes(&self) -> Option<VNode> {
134        self.inner.suspended_nodes.borrow_mut().take()
135    }
136
137    /// Check if the suspense boundary is resolved and frozen
138    pub fn frozen(&self) -> bool {
139        self.inner.frozen.get()
140    }
141
142    /// Resolve the suspense boundary on the server and freeze it to prevent future reruns of any child nodes of the suspense boundary
143    pub fn freeze(&self) {
144        self.inner.frozen.set(true);
145    }
146
147    /// Check if there are any suspended tasks
148    pub fn has_suspended_tasks(&self) -> bool {
149        !self.inner.suspended_tasks.borrow().is_empty()
150    }
151
152    /// Check if the suspense boundary is currently rendered as suspended
153    pub fn is_suspended(&self) -> bool {
154        self.inner.suspended_nodes.borrow().is_some()
155    }
156
157    /// Add a suspended task
158    pub(crate) fn add_suspended_task(&self, task: SuspendedFuture) {
159        self.inner.suspended_tasks.borrow_mut().push(task);
160        self.inner.id.get().needs_update();
161    }
162
163    /// Remove a suspended task
164    pub(crate) fn remove_suspended_task(&self, task: Task) {
165        self.inner
166            .suspended_tasks
167            .borrow_mut()
168            .retain(|t| t.task != task);
169        self.inner.id.get().needs_update();
170    }
171
172    /// Get all suspended tasks
173    pub fn suspended_futures(&self) -> Ref<[SuspendedFuture]> {
174        Ref::map(self.inner.suspended_tasks.borrow(), |tasks| {
175            tasks.as_slice()
176        })
177    }
178
179    /// Get the first suspended task with a loading placeholder
180    pub fn suspense_placeholder(&self) -> Option<Element> {
181        self.inner
182            .suspended_tasks
183            .borrow()
184            .iter()
185            .find_map(|task| task.suspense_placeholder())
186            .map(std::result::Result::Ok)
187    }
188
189    /// Run a closure after suspense is resolved
190    pub fn after_suspense_resolved(&self, callback: impl FnOnce() + 'static) {
191        let mut closures = self.inner.after_suspense_resolved.borrow_mut();
192        closures.push(Box::new(callback));
193    }
194
195    /// Run all closures that were queued to run after suspense is resolved
196    pub(crate) fn run_resolved_closures(&self, runtime: &Runtime) {
197        runtime.while_not_rendering(|| {
198            self.inner
199                .after_suspense_resolved
200                .borrow_mut()
201                .drain(..)
202                .for_each(|f| f());
203        })
204    }
205}
206
207/// A boundary that will capture any errors from child components
208pub struct SuspenseBoundaryInner {
209    suspended_tasks: RefCell<Vec<SuspendedFuture>>,
210    id: Cell<ScopeId>,
211    /// The nodes that are suspended under this boundary
212    suspended_nodes: RefCell<Option<VNode>>,
213    /// On the server, you can only resolve a suspense boundary once. This is used to track if the suspense boundary has been resolved and if it should be frozen
214    frozen: Cell<bool>,
215    /// Closures queued to run after the suspense boundary is resolved
216    after_suspense_resolved: RefCell<Vec<Box<dyn FnOnce()>>>,
217}
218
219impl Debug for SuspenseBoundaryInner {
220    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
221        f.debug_struct("SuspenseBoundaryInner")
222            .field("suspended_tasks", &self.suspended_tasks)
223            .field("id", &self.id)
224            .field("suspended_nodes", &self.suspended_nodes)
225            .field("frozen", &self.frozen)
226            .finish()
227    }
228}
229
230/// Provides context methods to [`Result<T, RenderError>`] to show loading indicators for suspended results
231///
232/// This trait is sealed and cannot be implemented outside of dioxus-core
233pub trait SuspenseExtension<T>: private::Sealed {
234    /// Add a loading indicator if the result is suspended
235    fn with_loading_placeholder(
236        self,
237        display_placeholder: impl FnOnce() -> Element,
238    ) -> std::result::Result<T, RenderError>;
239}
240
241impl<T> SuspenseExtension<T> for std::result::Result<T, RenderError> {
242    fn with_loading_placeholder(
243        self,
244        display_placeholder: impl FnOnce() -> Element,
245    ) -> std::result::Result<T, RenderError> {
246        if let Err(RenderError::Suspended(suspense)) = self {
247            Err(RenderError::Suspended(suspense.with_placeholder(
248                display_placeholder().unwrap_or_default(),
249            )))
250        } else {
251            self
252        }
253    }
254}
255
256pub(crate) mod private {
257    use super::*;
258
259    pub trait Sealed {}
260
261    impl<T> Sealed for std::result::Result<T, RenderError> {}
262}