sycamore_web/
resource.rs

1//! Async resources integrted with suspense.
2
3use std::future::Future;
4use std::ops::Deref;
5
6use futures::future::{FutureExt, LocalBoxFuture};
7use sycamore_futures::{SuspenseScope, SuspenseTaskGuard};
8
9use crate::*;
10
11/// Represents a asynchronous resource.
12#[derive(Clone, Copy)]
13pub struct Resource<T: 'static> {
14    /// The current value of the resource.
15    ///
16    /// This will initially be `None` while the resource is first fetched. For subsequent fetches,
17    /// the resource will still contain the previous value while the new value is fetched.
18    value: Signal<Option<T>>,
19    /// Whether the resource is currently loading or not.
20    is_loading: Signal<bool>,
21    /// The function that fetches the resource.
22    #[allow(clippy::complexity)]
23    refetch: Signal<Box<dyn FnMut() -> LocalBoxFuture<'static, T>>>,
24    /// A list of all the suspense scopes in which the resource is accessed.
25    scopes: Signal<Vec<SuspenseScope>>,
26    /// A list of suspense guards that are currently active.
27    guards: Signal<Vec<SuspenseTaskGuard>>,
28}
29
30impl<T: 'static> Resource<T> {
31    /// Create a new resource. By itself, this doesn't do anything.
32    fn new<F, Fut>(mut refetch: F) -> Self
33    where
34        F: FnMut() -> Fut + 'static,
35        Fut: Future<Output = T> + 'static,
36    {
37        Self {
38            value: create_signal(None),
39            is_loading: create_signal(true),
40            refetch: create_signal(Box::new(move || refetch().boxed_local())),
41            scopes: create_signal(Vec::new()),
42            guards: create_signal(Vec::new()),
43        }
44    }
45
46    /// Attach handlers to call the refetch function on the client side.
47    fn fetch_on_client(self) -> Self {
48        if is_not_ssr!() {
49            create_effect(move || {
50                self.is_loading.set(true);
51                // Take all the scopes and create a new guard.
52                for scope in self.scopes.take() {
53                    let guard = SuspenseTaskGuard::from_scope(scope);
54                    self.guards.update(|guards| guards.push(guard));
55                }
56
57                let fut = self.refetch.update_silent(|f| f());
58
59                sycamore_futures::create_suspense_task(async move {
60                    let value = fut.await;
61                    batch(move || {
62                        self.value.set(Some(value));
63                        self.is_loading.set(false);
64                        // Now, drop all the guards to resolve suspense.
65                        self.guards.update(|guards| guards.clear());
66                    });
67                });
68            })
69        }
70
71        self
72    }
73
74    /// Returns whether we are currently loading a new value or not.
75    pub fn is_loading(&self) -> bool {
76        self.is_loading.get()
77    }
78}
79
80/// Hijack deref so that we can track where the resource is being accessed.
81impl<T: 'static> Deref for Resource<T> {
82    type Target = ReadSignal<Option<T>>;
83
84    fn deref(&self) -> &Self::Target {
85        // If we are already loading, add a new suspense guard. Otherwise, register the scope so
86        // that we can create a new guard when loading.
87        if self.is_loading.get() {
88            let guard = SuspenseTaskGuard::new();
89            self.guards.update(|guards| guards.push(guard));
90        } else if let Some(scope) = try_use_context::<SuspenseScope>() {
91            self.scopes.update(|scopes| scopes.push(scope));
92        }
93
94        &self.value
95    }
96}
97
98/// Create a resrouce that will only be resolved on the client side.
99///
100/// If the resource has any dependencies, it is recommended to use [`on`] to make them explicit.
101/// This will ensure that the dependencies are tracked since reactive variables inside async
102/// contexts are not tracked automatically.
103///
104/// On the server, the resource will always be marked as loading.
105pub fn create_client_resource<F, Fut, T>(f: F) -> Resource<T>
106where
107    F: FnMut() -> Fut + 'static,
108    Fut: Future<Output = T> + 'static,
109    T: 'static,
110{
111    Resource::new(f).fetch_on_client()
112}