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 }),
106 }
107 }
108
109 /// Mount the context in a specific scope
110 pub(crate) fn mount(&self, scope: ScopeId) {
111 self.inner.id.set(scope);
112 }
113
114 /// Get the suspense boundary's suspended nodes
115 pub fn suspended_nodes(&self) -> Option<VNode> {
116 self.inner
117 .suspended_nodes
118 .borrow()
119 .as_ref()
120 .map(|node| node.clone())
121 }
122
123 /// Set the suspense boundary's suspended nodes
124 pub(crate) fn set_suspended_nodes(&self, suspended_nodes: VNode) {
125 self.inner
126 .suspended_nodes
127 .borrow_mut()
128 .replace(suspended_nodes);
129 }
130
131 /// Take the suspense boundary's suspended nodes
132 pub(crate) fn take_suspended_nodes(&self) -> Option<VNode> {
133 self.inner.suspended_nodes.borrow_mut().take()
134 }
135
136 /// Check if the suspense boundary is resolved and frozen
137 pub fn frozen(&self) -> bool {
138 self.inner.frozen.get()
139 }
140
141 /// Resolve the suspense boundary on the server and freeze it to prevent future reruns of any child nodes of the suspense boundary
142 pub fn freeze(&self) {
143 self.inner.frozen.set(true);
144 }
145
146 /// Check if there are any suspended tasks
147 pub fn has_suspended_tasks(&self) -> bool {
148 !self.inner.suspended_tasks.borrow().is_empty()
149 }
150
151 /// Check if the suspense boundary is currently rendered as suspended
152 pub fn is_suspended(&self) -> bool {
153 self.inner.suspended_nodes.borrow().is_some()
154 }
155
156 /// Add a suspended task
157 pub(crate) fn add_suspended_task(&self, task: SuspendedFuture) {
158 self.inner.suspended_tasks.borrow_mut().push(task);
159 self.inner.id.get().needs_update();
160 }
161
162 /// Remove a suspended task
163 pub(crate) fn remove_suspended_task(&self, task: Task) {
164 self.inner
165 .suspended_tasks
166 .borrow_mut()
167 .retain(|t| t.task != task);
168 self.inner.id.get().needs_update();
169 }
170
171 /// Get all suspended tasks
172 pub fn suspended_futures(&self) -> Ref<[SuspendedFuture]> {
173 Ref::map(self.inner.suspended_tasks.borrow(), |tasks| {
174 tasks.as_slice()
175 })
176 }
177
178 /// Get the first suspended task with a loading placeholder
179 pub fn suspense_placeholder(&self) -> Option<Element> {
180 self.inner
181 .suspended_tasks
182 .borrow()
183 .iter()
184 .find_map(|task| task.suspense_placeholder())
185 .map(std::result::Result::Ok)
186 }
187}
188
189/// A boundary that will capture any errors from child components
190#[derive(Debug)]
191pub struct SuspenseBoundaryInner {
192 suspended_tasks: RefCell<Vec<SuspendedFuture>>,
193 id: Cell<ScopeId>,
194 /// The nodes that are suspended under this boundary
195 suspended_nodes: RefCell<Option<VNode>>,
196 /// 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
197 frozen: Cell<bool>,
198}
199
200/// Provides context methods to [`Result<T, RenderError>`] to show loading indicators for suspended results
201///
202/// This trait is sealed and cannot be implemented outside of dioxus-core
203pub trait SuspenseExtension<T>: private::Sealed {
204 /// Add a loading indicator if the result is suspended
205 fn with_loading_placeholder(
206 self,
207 display_placeholder: impl FnOnce() -> Element,
208 ) -> std::result::Result<T, RenderError>;
209}
210
211impl<T> SuspenseExtension<T> for std::result::Result<T, RenderError> {
212 fn with_loading_placeholder(
213 self,
214 display_placeholder: impl FnOnce() -> Element,
215 ) -> std::result::Result<T, RenderError> {
216 if let Err(RenderError::Suspended(suspense)) = self {
217 Err(RenderError::Suspended(suspense.with_placeholder(
218 display_placeholder().unwrap_or_default(),
219 )))
220 } else {
221 self
222 }
223 }
224}
225
226pub(crate) mod private {
227 use super::*;
228
229 pub trait Sealed {}
230
231 impl<T> Sealed for std::result::Result<T, RenderError> {}
232}