Skip to main content

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