dioxus_fullstack_core/
loader.rs

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