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.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        self().into_dyn_node()
267    }
268}
269
270impl<T: 'static> PartialEq for Loader<T> {
271    fn eq(&self, other: &Self) -> bool {
272        self.read_value == other.read_value
273    }
274}
275
276impl<T: Clone> Deref for Loader<T>
277where
278    T: PartialEq + 'static,
279{
280    type Target = dyn Fn() -> T;
281
282    fn deref(&self) -> &Self::Target {
283        unsafe { ReadableExt::deref_impl(self) }
284    }
285}
286
287read_impls!(Loader<T> where T: PartialEq);
288
289impl<T> Clone for Loader<T> {
290    fn clone(&self) -> Self {
291        *self
292    }
293}
294
295impl<T> Copy for Loader<T> {}
296
297#[derive(Clone, Copy, PartialEq, Hash, Eq, Debug)]
298pub enum LoaderState {
299    /// The loader's future is still running
300    Pending,
301
302    /// The loader's future has completed successfully
303    Ready,
304
305    /// The loader's future has failed and now the loader is in an error state.
306    Failed,
307}
308
309#[derive(PartialEq)]
310pub struct LoaderHandle<M = ()> {
311    resource: Resource<()>,
312    error: Signal<Option<CapturedError>>,
313    state: Signal<LoaderState>,
314    _marker: std::marker::PhantomData<M>,
315}
316
317impl LoaderHandle {
318    /// Restart the loading task.
319    pub fn restart(&mut self) {
320        self.resource.restart();
321    }
322
323    /// Get the current state of the loader.
324    pub fn state(&self) -> LoaderState {
325        *self.state.read()
326    }
327
328    pub fn error(&self) -> Option<CapturedError> {
329        self.error.read().as_ref().cloned()
330    }
331}
332
333impl Clone for LoaderHandle {
334    fn clone(&self) -> Self {
335        *self
336    }
337}
338
339impl Copy for LoaderHandle {}
340
341#[derive(PartialEq)]
342pub enum Loading {
343    /// The loader is still pending and the component should suspend.
344    Pending(LoaderHandle),
345
346    /// The loader has failed and an error will be returned up the tree.
347    Failed(LoaderHandle),
348}
349
350impl std::fmt::Debug for Loading {
351    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
352        match self {
353            Loading::Pending(_) => write!(f, "Loading::Pending"),
354            Loading::Failed(_) => write!(f, "Loading::Failed"),
355        }
356    }
357}
358
359impl std::fmt::Display for Loading {
360    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
361        match self {
362            Loading::Pending(_) => write!(f, "Loading is still pending"),
363            Loading::Failed(_) => write!(f, "Loading has failed"),
364        }
365    }
366}
367
368/// Convert a Loading into a RenderError for use with the `?` operator in components
369impl From<Loading> for RenderError {
370    fn from(val: Loading) -> Self {
371        match val {
372            Loading::Pending(t) => RenderError::Suspended(SuspendedFuture::new(t.resource.task())),
373            Loading::Failed(err) => RenderError::Error(
374                err.error
375                    .cloned()
376                    .expect("LoaderHandle in Failed state should always have an error"),
377            ),
378        }
379    }
380}