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: TaskId,
41}
42
43impl SuspendedFuture {
44    /// Create a new suspended future
45    pub fn new(task: Task) -> Self {
46        Self {
47            task: task.id,
48            origin: current_scope_id(),
49        }
50    }
51
52    /// Get the task that was suspended
53    pub fn task(&self) -> Task {
54        Task::from_id(self.task)
55    }
56
57    /// Create a deep clone of this suspended future
58    pub(crate) fn deep_clone(&self) -> Self {
59        Self {
60            task: self.task,
61            origin: self.origin,
62        }
63    }
64}
65
66impl std::fmt::Display for SuspendedFuture {
67    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
68        write!(f, "SuspendedFuture {{ task: {:?} }}", self.task)
69    }
70}
71
72/// A context with information about suspended components
73#[derive(Debug, Clone)]
74pub struct SuspenseContext {
75    inner: Rc<SuspenseBoundaryInner>,
76}
77
78impl PartialEq for SuspenseContext {
79    fn eq(&self, other: &Self) -> bool {
80        Rc::ptr_eq(&self.inner, &other.inner)
81    }
82}
83
84impl SuspenseContext {
85    /// Create a new suspense boundary in a specific scope
86    pub(crate) fn new() -> Self {
87        Self {
88            inner: Rc::new(SuspenseBoundaryInner {
89                rt: Runtime::current(),
90                suspended_tasks: RefCell::new(vec![]),
91                id: Cell::new(ScopeId::ROOT),
92                suspended_nodes: Default::default(),
93                frozen: Default::default(),
94                after_suspense_resolved: Default::default(),
95            }),
96        }
97    }
98
99    /// Mount the context in a specific scope
100    pub(crate) fn mount(&self, scope: ScopeId) {
101        self.inner.id.set(scope);
102    }
103
104    /// Get the suspense boundary's suspended nodes
105    pub fn suspended_nodes(&self) -> Option<VNode> {
106        self.inner
107            .suspended_nodes
108            .borrow()
109            .as_ref()
110            .map(|node| node.clone())
111    }
112
113    /// Set the suspense boundary's suspended nodes
114    pub(crate) fn set_suspended_nodes(&self, suspended_nodes: VNode) {
115        self.inner
116            .suspended_nodes
117            .borrow_mut()
118            .replace(suspended_nodes);
119    }
120
121    /// Take the suspense boundary's suspended nodes
122    pub(crate) fn take_suspended_nodes(&self) -> Option<VNode> {
123        self.inner.suspended_nodes.borrow_mut().take()
124    }
125
126    /// Check if the suspense boundary is resolved and frozen
127    pub fn frozen(&self) -> bool {
128        self.inner.frozen.get()
129    }
130
131    /// Resolve the suspense boundary on the server and freeze it to prevent future reruns of any child nodes of the suspense boundary
132    pub fn freeze(&self) {
133        self.inner.frozen.set(true);
134    }
135
136    /// Check if there are any suspended tasks
137    pub fn has_suspended_tasks(&self) -> bool {
138        !self.inner.suspended_tasks.borrow().is_empty()
139    }
140
141    /// Check if the suspense boundary is currently rendered as suspended
142    pub fn is_suspended(&self) -> bool {
143        self.inner.suspended_nodes.borrow().is_some()
144    }
145
146    /// Add a suspended task
147    pub(crate) fn add_suspended_task(&self, task: SuspendedFuture) {
148        self.inner.suspended_tasks.borrow_mut().push(task);
149        self.inner.rt.needs_update(self.inner.id.get());
150    }
151
152    /// Remove a suspended task
153    pub(crate) fn remove_suspended_task(&self, task: Task) {
154        self.inner
155            .suspended_tasks
156            .borrow_mut()
157            .retain(|t| t.task != task.id);
158        self.inner.rt.needs_update(self.inner.id.get());
159    }
160
161    /// Get all suspended tasks
162    pub fn suspended_futures(&self) -> Ref<'_, [SuspendedFuture]> {
163        Ref::map(self.inner.suspended_tasks.borrow(), |tasks| {
164            tasks.as_slice()
165        })
166    }
167
168    /// Run a closure after suspense is resolved
169    pub fn after_suspense_resolved(&self, callback: impl FnOnce() + 'static) {
170        let mut closures = self.inner.after_suspense_resolved.borrow_mut();
171        closures.push(Box::new(callback));
172    }
173
174    /// Run all closures that were queued to run after suspense is resolved
175    pub(crate) fn run_resolved_closures(&self, runtime: &Runtime) {
176        runtime.while_not_rendering(|| {
177            self.inner
178                .after_suspense_resolved
179                .borrow_mut()
180                .drain(..)
181                .for_each(|f| f());
182        })
183    }
184}
185
186/// A boundary that will capture any errors from child components
187pub struct SuspenseBoundaryInner {
188    rt: Rc<Runtime>,
189
190    suspended_tasks: RefCell<Vec<SuspendedFuture>>,
191
192    id: Cell<ScopeId>,
193
194    /// The nodes that are suspended under this boundary
195    suspended_nodes: RefCell<Option<VNode>>,
196
197    /// 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
198    frozen: Cell<bool>,
199
200    /// Closures queued to run after the suspense boundary is resolved
201    after_suspense_resolved: RefCell<Vec<Box<dyn FnOnce()>>>,
202}
203
204impl Debug for SuspenseBoundaryInner {
205    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
206        f.debug_struct("SuspenseBoundaryInner")
207            .field("suspended_tasks", &self.suspended_tasks)
208            .field("id", &self.id)
209            .field("suspended_nodes", &self.suspended_nodes)
210            .field("frozen", &self.frozen)
211            .finish()
212    }
213}