dioxus_fullstack_core/
loader.rs

1use dioxus_core::{use_hook, IntoAttributeValue, IntoDynNode, Subscribers};
2use dioxus_core::{CapturedError, RenderError, Result, SuspendedFuture};
3use dioxus_hooks::{use_resource, use_signal, Resource};
4use dioxus_signals::{
5    read_impls, ReadSignal, Readable, ReadableBoxExt, ReadableExt, ReadableRef, Signal, Writable,
6    WritableExt, WriteLock,
7};
8use generational_box::{BorrowResult, UnsyncStorage};
9use serde::{de::DeserializeOwned, Serialize};
10use std::ops::Deref;
11use std::{cmp::PartialEq, future::Future};
12
13/// A hook to create a resource that loads data asynchronously.
14///
15/// This hook takes a closure that returns a future. This future will be executed on both the client
16/// and the server. The loader will return `Loading` until the future resolves, at which point it will
17/// return a `Loader<T>`. If the future fails, it will return `Loading::Failed`.
18///
19/// After the loader has successfully loaded once, it will never suspend the component again, but will
20/// instead re-load the value in the background whenever any of its dependencies change.
21///
22/// If an error occurs while re-loading, `use_loader` will once again emit a `Loading::Failed` value.
23/// The `use_loader` hook will never return a suspended state after the initial load.
24///
25/// # On the server
26///
27/// On the server, this hook will block the rendering of the component (and therefore, the page) until
28/// the future resolves. Any server futures called by `use_loader` will receive the same request context
29/// as the component that called `use_loader`.
30#[allow(clippy::result_large_err)]
31#[track_caller]
32pub fn use_loader<F, T, E>(mut future: impl FnMut() -> F + 'static) -> Result<Loader<T>, Loading>
33where
34    F: Future<Output = Result<T, E>> + 'static,
35    T: 'static + PartialEq + Serialize + DeserializeOwned,
36    E: Into<CapturedError> + 'static,
37{
38    let serialize_context = use_hook(crate::transport::serialize_context);
39
40    // We always create a storage entry, even if the data isn't ready yet to make it possible to deserialize pending server futures on the client
41    #[allow(unused)]
42    let storage_entry: crate::transport::SerializeContextEntry<Result<T, CapturedError>> =
43        use_hook(|| serialize_context.create_entry());
44
45    #[cfg(feature = "server")]
46    let caller = std::panic::Location::caller();
47
48    // If this is the first run and we are on the web client, the data might be cached
49    #[cfg(feature = "web")]
50    let initial_web_result =
51        use_hook(|| std::rc::Rc::new(std::cell::RefCell::new(Some(storage_entry.get()))));
52
53    let mut error = use_signal(|| None as Option<CapturedError>);
54    let mut value = use_signal(|| None as Option<T>);
55    let mut loader_state = use_signal(|| LoaderState::Pending);
56
57    let resource = use_resource(move || {
58        #[cfg(feature = "server")]
59        let storage_entry = storage_entry.clone();
60
61        let user_fut = future();
62
63        #[cfg(feature = "web")]
64        let initial_web_result = initial_web_result.clone();
65
66        #[allow(clippy::let_and_return)]
67        async move {
68            // If this is the first run and we are on the web client, the data might be cached
69            #[cfg(feature = "web")]
70            match initial_web_result.take() {
71                // The data was deserialized successfully from the server
72                Some(Ok(o)) => {
73                    match o {
74                        Ok(v) => {
75                            value.set(Some(v));
76                            loader_state.set(LoaderState::Ready);
77                        }
78                        Err(e) => {
79                            error.set(Some(e));
80                            loader_state.set(LoaderState::Failed);
81                        }
82                    };
83                    return;
84                }
85
86                // The data is still pending from the server. Don't try to resolve it on the client
87                Some(Err(crate::transport::TakeDataError::DataPending)) => {
88                    std::future::pending::<()>().await
89                }
90
91                // The data was not available on the server, rerun the future
92                Some(Err(_)) => {}
93
94                // This isn't the first run, so we don't need do anything
95                None => {}
96            }
97
98            // Otherwise just run the future itself
99            let out = user_fut.await;
100
101            // Remap the error to the captured error type so it's cheap to clone and pass out, just
102            // slightly more cumbersome to access the inner error.
103            let out = out.map_err(|e| {
104                let anyhow_err: CapturedError = e.into();
105                anyhow_err
106            });
107
108            // If this is the first run and we are on the server, cache the data in the slot we reserved for it
109            #[cfg(feature = "server")]
110            storage_entry.insert(&out, caller);
111
112            match out {
113                Ok(v) => {
114                    value.set(Some(v));
115                    loader_state.set(LoaderState::Ready);
116                }
117                Err(e) => {
118                    error.set(Some(e));
119                    loader_state.set(LoaderState::Failed);
120                }
121            };
122        }
123    });
124
125    // On the first run, force this task to be polled right away in case its value is ready
126    use_hook(|| {
127        let _ = resource.task().poll_now();
128    });
129
130    let read_value = use_hook(|| value.map(|f| f.as_ref().unwrap()).boxed());
131
132    let handle = LoaderHandle {
133        resource,
134        error,
135        state: loader_state,
136        _marker: std::marker::PhantomData,
137    };
138
139    match &*loader_state.read_unchecked() {
140        LoaderState::Pending => Err(Loading::Pending(handle)),
141        LoaderState::Failed => Err(Loading::Failed(handle)),
142        LoaderState::Ready => Ok(Loader {
143            real_value: value,
144            read_value,
145            error,
146            state: loader_state,
147            handle,
148        }),
149    }
150}
151
152/// A Loader is a signal that represents a value that is loaded asynchronously.
153///
154/// Once a `Loader<T>` has been successfully created from `use_loader`, it can be use like a normal signal of type `T`.
155///
156/// When the loader is re-reloading its values, it will no longer suspend its component, making it
157/// very useful for server-side-rendering.
158pub struct Loader<T: 'static> {
159    /// This is a signal that unwraps the inner value. We can't give it out unless we know the inner value is Some(T)!
160    read_value: ReadSignal<T>,
161
162    /// This is the actual signal. We let the user set this value if they want to, but we can't let them set it to `None`.
163    real_value: Signal<Option<T>>,
164    error: Signal<Option<CapturedError>>,
165    state: Signal<LoaderState>,
166    handle: LoaderHandle,
167}
168
169impl<T: 'static> Loader<T> {
170    /// Get the error that occurred during loading, if any.
171    ///
172    /// After initial load, this will return `None` until the next reload fails.
173    pub fn error(&self) -> Option<CapturedError> {
174        self.error.read().as_ref().cloned()
175    }
176
177    /// Restart the loading task.
178    ///
179    /// After initial load, this won't suspend the component, but will reload in the background.
180    pub fn restart(&mut self) {
181        self.handle.restart();
182    }
183
184    /// Check if the loader has failed.
185    pub fn is_error(&self) -> bool {
186        self.error.read().is_some() && matches!(*self.state.read(), LoaderState::Failed)
187    }
188
189    /// Cancel the current loading task.
190    pub fn cancel(&mut self) {
191        self.handle.resource.cancel();
192    }
193
194    pub fn loading(&self) -> bool {
195        !self.handle.resource.finished()
196    }
197}
198
199impl<T: 'static> Writable for Loader<T> {
200    type WriteMetadata = <Signal<Option<T>> as Writable>::WriteMetadata;
201
202    fn try_write_unchecked(
203        &self,
204    ) -> std::result::Result<
205        dioxus_signals::WritableRef<'static, Self>,
206        generational_box::BorrowMutError,
207    >
208    where
209        Self::Target: 'static,
210    {
211        let writer = self.real_value.try_write_unchecked()?;
212        Ok(WriteLock::map(writer, |f: &mut Option<T>| {
213            f.as_mut()
214                .expect("Loader value should be set if the `Loader<T>` exists")
215        }))
216    }
217}
218
219impl<T> Readable for Loader<T> {
220    type Target = T;
221    type Storage = UnsyncStorage;
222
223    #[track_caller]
224    fn try_read_unchecked(
225        &self,
226    ) -> Result<ReadableRef<'static, Self>, generational_box::BorrowError>
227    where
228        T: 'static,
229    {
230        Ok(self.read_value.read_unchecked())
231    }
232
233    /// Get the current value of the signal. **Unlike read, this will not subscribe the current scope to the signal which can cause parts of your UI to not update.**
234    ///
235    /// If the signal has been dropped, this will panic.
236    #[track_caller]
237    fn try_peek_unchecked(&self) -> BorrowResult<ReadableRef<'static, Self>>
238    where
239        T: 'static,
240    {
241        Ok(self.read_value.peek_unchecked())
242    }
243
244    fn subscribers(&self) -> Subscribers
245    where
246        T: 'static,
247    {
248        self.read_value.subscribers()
249    }
250}
251
252impl<T> IntoAttributeValue for Loader<T>
253where
254    T: Clone + IntoAttributeValue + PartialEq + 'static,
255{
256    fn into_value(self) -> dioxus_core::AttributeValue {
257        self.with(|f| f.clone().into_value())
258    }
259}
260
261impl<T> IntoDynNode for Loader<T>
262where
263    T: Clone + IntoDynNode + PartialEq + 'static,
264{
265    fn into_dyn_node(self) -> dioxus_core::DynamicNode {
266        let t: T = self();
267        t.into_dyn_node()
268    }
269}
270
271impl<T: 'static> PartialEq for Loader<T> {
272    fn eq(&self, other: &Self) -> bool {
273        self.read_value == other.read_value
274    }
275}
276
277impl<T: Clone> Deref for Loader<T>
278where
279    T: PartialEq + 'static,
280{
281    type Target = dyn Fn() -> T;
282
283    fn deref(&self) -> &Self::Target {
284        unsafe { ReadableExt::deref_impl(self) }
285    }
286}
287
288read_impls!(Loader<T> where T: PartialEq);
289
290impl<T> Clone for Loader<T> {
291    fn clone(&self) -> Self {
292        *self
293    }
294}
295
296impl<T> Copy for Loader<T> {}
297
298#[derive(Clone, Copy, PartialEq, Hash, Eq, Debug)]
299pub enum LoaderState {
300    /// The loader's future is still running
301    Pending,
302
303    /// The loader's future has completed successfully
304    Ready,
305
306    /// The loader's future has failed and now the loader is in an error state.
307    Failed,
308}
309
310#[derive(PartialEq)]
311pub struct LoaderHandle<M = ()> {
312    resource: Resource<()>,
313    error: Signal<Option<CapturedError>>,
314    state: Signal<LoaderState>,
315    _marker: std::marker::PhantomData<M>,
316}
317
318impl LoaderHandle {
319    /// Restart the loading task.
320    pub fn restart(&mut self) {
321        self.resource.restart();
322    }
323
324    /// Get the current state of the loader.
325    pub fn state(&self) -> LoaderState {
326        *self.state.read()
327    }
328
329    pub fn error(&self) -> Option<CapturedError> {
330        self.error.read().as_ref().cloned()
331    }
332}
333
334impl Clone for LoaderHandle {
335    fn clone(&self) -> Self {
336        *self
337    }
338}
339
340impl Copy for LoaderHandle {}
341
342#[derive(PartialEq)]
343pub enum Loading {
344    /// The loader is still pending and the component should suspend.
345    Pending(LoaderHandle),
346
347    /// The loader has failed and an error will be returned up the tree.
348    Failed(LoaderHandle),
349}
350
351impl std::fmt::Debug for Loading {
352    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
353        match self {
354            Loading::Pending(_) => write!(f, "Loading::Pending"),
355            Loading::Failed(_) => write!(f, "Loading::Failed"),
356        }
357    }
358}
359
360impl std::fmt::Display for Loading {
361    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
362        match self {
363            Loading::Pending(_) => write!(f, "Loading is still pending"),
364            Loading::Failed(_) => write!(f, "Loading has failed"),
365        }
366    }
367}
368
369/// Convert a Loading into a RenderError for use with the `?` operator in components
370impl From<Loading> for RenderError {
371    fn from(val: Loading) -> Self {
372        match val {
373            Loading::Pending(t) => RenderError::Suspended(SuspendedFuture::new(t.resource.task())),
374            Loading::Failed(err) => RenderError::Error(
375                err.error
376                    .cloned()
377                    .expect("LoaderHandle in Failed state should always have an error"),
378            ),
379        }
380    }
381}