1//! Suspense with first class `async`/`await` support.
2//!
3//! The [`Suspense`] component is used to "suspend" execution and wait until async tasks are
4//! finished before rendering.
56use futures::channel::oneshot;
7use futures::Future;
8use sycamore_reactive::*;
910use crate::*;
1112/// A context value that keeps track of all the signals representing the number of tasks remaining
13/// in a suspense scope.
14///
15/// This is useful for figuring out when all suspense tasks are completed on the page.
16#[derive(Copy, Clone, Debug, Default)]
17struct AllTasksRemaining {
18 all_tasks_remaining: Signal<Vec<Signal<u32>>>,
19}
2021/// Represents a new suspense scope. This is created by a call to [`create_suspense_scope`].
22#[derive(Copy, Clone, Debug)]
23pub struct SuspenseScope {
24 tasks_remaining: Signal<u32>,
25/// The parent suspense scope of the current scope, if it exists.
26pub parent: Option<Signal<SuspenseScope>>,
27/// Signal that is set to `true` when the view is rendered and streamed into the buffer.
28 /// This is unused on the client side.
29pub sent: Signal<bool>,
30}
3132impl SuspenseScope {
33/// Create a new suspense scope, optionally with a parent scope.
34 ///
35 /// The parent scope should always be located in a reactive scope that is an ancestor of
36 /// this scope.
37pub fn new(parent: Option<SuspenseScope>) -> Self {
38let tasks_remaining = create_signal(0);
39let global = use_global_scope().run_in(|| use_context_or_else(AllTasksRemaining::default));
40 global
41 .all_tasks_remaining
42 .update(|vec| vec.push(tasks_remaining));
43Self {
44 tasks_remaining,
45 parent: parent.map(create_signal),
46 sent: create_signal(false),
47 }
48 }
4950/// Implementation for [`Self::is_loading`]. This is used to recursively check whether we are
51 /// loading or not.
52fn _is_loading(self) -> bool {
53self.tasks_remaining.get() > 0
54|| self
55.parent
56 .as_ref()
57 .map_or(false, |parent| parent.get()._is_loading())
58 }
5960/// Returns a signal representing whether we are currently loading this suspense or not.
61 ///
62 /// Implementation for the [`use_is_loading`] hook.
63pub fn is_loading(self) -> ReadSignal<bool> {
64 create_selector(move || self._is_loading())
65 }
6667/// Returns a future that resolves once the scope is no longer loading.
68pub async fn until_finished(self) {
69let (tx, rx) = oneshot::channel();
70let mut tx = Some(tx);
71 create_effect(move || {
72if !self._is_loading() {
73if let Some(tx) = tx.take() {
74 tx.send(()).unwrap();
75 }
76 }
77 });
7879 rx.await.unwrap()
80 }
81}
8283/// A guard that keeps a suspense scope suspended until it is dropped.
84#[derive(Debug)]
85pub struct SuspenseTaskGuard {
86 scope: Option<SuspenseScope>,
87}
8889impl SuspenseTaskGuard {
90/// Creates a new suspense task guard. This will suspend the current suspense scope until this
91 /// guard is dropped.
92pub fn new() -> Self {
93let scope = try_use_context::<SuspenseScope>();
94if let Some(mut scope) = scope {
95 scope.tasks_remaining += 1;
96 }
97Self { scope }
98 }
99100/// Create a new suspense task guard from a suspense scope.
101pub fn from_scope(mut scope: SuspenseScope) -> Self {
102 scope.tasks_remaining += 1;
103Self { scope: Some(scope) }
104 }
105}
106107impl Default for SuspenseTaskGuard {
108fn default() -> Self {
109Self::new()
110 }
111}
112113impl Drop for SuspenseTaskGuard {
114fn drop(&mut self) {
115if let Some(mut scope) = self.scope {
116 scope.tasks_remaining -= 1;
117 }
118 }
119}
120121/// Creates a new task that is to be tracked by the suspense system.
122///
123/// This is used to signal to a `Suspense` component higher up in the component hierarchy that
124/// there is some async task that should be awaited before showing the UI.
125///
126/// If this is called from outside a suspense scope, the task will be executed normally.
127pub fn create_suspense_task(f: impl Future<Output = ()> + 'static) {
128let guard = SuspenseTaskGuard::new();
129 spawn_local_scoped(async move {
130 f.await;
131 drop(guard);
132 });
133}
134135/// Create a new suspense scope that is detatched from the rest of the suspense hierarchy.
136///
137/// This is useful if you want the result of this suspense to be independent of the praent suspense
138/// scope.
139///
140/// It is rarely recommended to use this fucntion as it can lead to unexpected behavior when using
141/// server side rendering, and in particular, streaming. Instead, use [`create_suspense_scope`].
142///
143/// The reason for this is because we generally expect outer suspenses to be resolved first before
144/// an inner suspense is resolved, since otherwise we would have no place to show the inner suspense
145/// as the outer fallback is still being displayed.
146pub fn create_detatched_suspense_scope<T>(f: impl FnOnce() -> T) -> (T, SuspenseScope) {
147let scope = SuspenseScope::new(None);
148 provide_context_in_new_scope(scope, move || {
149let ret = f();
150 (ret, scope)
151 })
152}
153154/// Calls the given function and registers all suspense tasks.
155///
156/// Returns a tuple containing the return value of the function and the created suspense scope.
157///
158/// If this is called inside another call to [`await_suspense`], this suspense will wait until the
159/// parent suspense is resolved.
160pub fn create_suspense_scope<T>(f: impl FnOnce() -> T) -> (T, SuspenseScope) {
161let parent = try_use_context::<SuspenseScope>();
162let scope = SuspenseScope::new(parent);
163 provide_context_in_new_scope(scope, move || {
164let ret = f();
165 (ret, scope)
166 })
167}
168169/// Waits until all suspense task in current scope are completed.
170///
171/// Does not create a new suspense scope.
172///
173/// If not called inside a suspense scope, the future will resolve immediately.
174pub async fn await_suspense_current() {
175if let Some(scope) = try_use_context::<SuspenseScope>() {
176 scope.until_finished().await;
177 }
178}
179180/// Returns a signal representing whether we are currently loading this suspense or not.
181///
182/// This will be true if there are any tasks remaining in this scope or in any parent
183/// scope.
184///
185/// This function is also reactive and so the loading state can be tracked. If it is called outside
186/// of a suspense scope, the signal will always be `false`.
187pub fn use_is_loading() -> ReadSignal<bool> {
188 try_use_context::<SuspenseScope>().map_or(*create_signal(false), |scope| scope.is_loading())
189}
190191/// Returns whether any suspense scope is current loading.
192///
193/// This is unlike [`use_is_loading`] in that it can be called outside of a suspense scope and does
194/// not apply to any suspense scope in particular.
195pub fn use_is_loading_global() -> bool {
196if let Some(global) = try_use_context::<AllTasksRemaining>() {
197 global
198 .all_tasks_remaining
199 .with(|vec| vec.iter().any(|signal| signal.get() > 0))
200 } else {
201false
202}
203}
204205#[cfg(test)]
206mod tests {
207use std::cell::Cell;
208use std::rc::Rc;
209210use super::*;
211212#[test]
213fn suspense_scope() {
214let _ = create_root(|| {
215let _ = create_suspense_scope(|| {
216let outer_scope = try_use_context::<SuspenseScope>();
217assert!(outer_scope.is_some());
218assert!(outer_scope.unwrap().parent.is_none());
219220let _ = create_suspense_scope(|| {
221let inner_scope = try_use_context::<SuspenseScope>();
222assert!(inner_scope.is_some());
223assert!(inner_scope.unwrap().parent.is_some());
224 });
225 });
226 });
227 }
228229#[tokio::test]
230async fn suspense_await_suspense() {
231let (tx, rx) = oneshot::channel();
232let is_completed = Rc::new(Cell::new(false));
233234let local = tokio::task::LocalSet::new();
235 local
236 .run_until(async {
237let _ = create_root({
238let is_completed = is_completed.clone();
239 || {
240 spawn_local_scoped(async move {
241let (_, scope) = create_suspense_scope(|| {
242 create_suspense_task(async move {
243 rx.await.unwrap();
244 });
245 });
246247 scope.until_finished().await;
248 is_completed.set(true);
249 });
250 }
251 });
252 })
253 .await;
254255assert!(!is_completed.get());
256257 tx.send(()).unwrap();
258 local.await;
259assert!(is_completed.get());
260 }
261262#[tokio::test]
263async fn use_is_loading_global_works() {
264let (tx, rx) = oneshot::channel();
265266let local = tokio::task::LocalSet::new();
267let mut root = create_root(|| {});
268 local
269 .run_until(async {
270 root = create_root(|| {
271let _ = create_suspense_scope(|| {
272 create_suspense_task(async move {
273 rx.await.unwrap();
274 });
275 });
276 });
277 })
278 .await;
279280 root.run_in(|| {
281assert!(use_is_loading_global());
282 });
283284 tx.send(()).unwrap();
285 local.await;
286287 root.run_in(|| {
288assert!(!use_is_loading_global());
289 });
290 }
291}