Skip to main content

dioxus_hooks/
use_resource.rs

1#![allow(missing_docs)]
2
3use crate::{UseWaker, use_callback, use_signal, use_waker};
4
5use dioxus_core::{
6    Callback, IntoAttributeValue, IntoDynNode, ReactiveContext, RenderError, Subscribers,
7    SuspendedFuture, Task, spawn, use_hook,
8};
9use dioxus_signals::*;
10use futures_util::{
11    FutureExt, StreamExt,
12    future::{self},
13    pin_mut,
14};
15use std::{cell::Cell, future::Future, rc::Rc};
16use std::{fmt::Debug, ops::Deref};
17
18#[doc = include_str!("../docs/use_resource.md")]
19#[doc = include_str!("../docs/rules_of_hooks.md")]
20#[doc = include_str!("../docs/moving_state_around.md")]
21#[doc(alias = "use_async_memo")]
22#[doc(alias = "use_memo_async")]
23#[track_caller]
24pub fn use_resource<T, F>(mut future: impl FnMut() -> F + 'static) -> Resource<T>
25where
26    T: 'static,
27    F: Future<Output = T> + 'static,
28{
29    let location = std::panic::Location::caller();
30
31    let mut value = use_signal(|| None);
32    let mut state = use_signal(|| UseResourceState::Pending);
33    let (rc, changed) = use_hook(|| {
34        let (rc, changed) = ReactiveContext::new_with_origin(location);
35        (rc, Rc::new(Cell::new(Some(changed))))
36    });
37
38    let mut waker = use_waker::<()>();
39
40    let cb = use_callback(move |_| {
41        // Set the state to Pending when the task is restarted
42        state.set(UseResourceState::Pending);
43
44        // Create the user's task
45        let fut = rc.reset_and_run_in(&mut future);
46
47        // Spawn a wrapper task that polls the inner future and watches its dependencies
48        spawn(async move {
49            // Move the future here and pin it so we can poll it
50            let fut = fut;
51            pin_mut!(fut);
52
53            // Run each poll in the context of the reactive scope
54            // This ensures the scope is properly subscribed to the future's dependencies
55            let res = future::poll_fn(|cx| {
56                rc.run_in(|| {
57                    tracing::trace_span!("polling resource", location = %location)
58                        .in_scope(|| fut.poll_unpin(cx))
59                })
60            })
61            .await;
62
63            // Set the value and state
64            state.set(UseResourceState::Ready);
65            value.set(Some(res));
66
67            // Notify that the value has changed
68            waker.wake(());
69        })
70    });
71
72    let mut task = use_hook(|| Signal::new(cb(())));
73
74    use_hook(|| {
75        let mut changed = changed.take().unwrap();
76        spawn(async move {
77            loop {
78                // Wait for the dependencies to change
79                let _ = changed.next().await;
80
81                // Stop the old task
82                task.write().cancel();
83
84                // Start a new task
85                task.set(cb(()));
86            }
87        })
88    });
89
90    Resource {
91        task,
92        value,
93        state,
94        waker,
95        callback: cb,
96    }
97}
98
99/// A handle to a reactive future spawned with [`use_resource`] that can be used to modify or read the result of the future.
100///
101/// ## Example
102///
103/// Reading the result of a resource:
104/// ```rust, no_run
105/// # use dioxus::prelude::*;
106/// # use std::time::Duration;
107/// fn App() -> Element {
108///     let mut revision = use_signal(|| "1d03b42");
109///     let mut resource = use_resource(move || async move {
110///         // This will run every time the revision signal changes because we read the count inside the future
111///         reqwest::get(format!("https://github.com/DioxusLabs/awesome-dioxus/blob/{revision}/awesome.json")).await
112///     });
113///
114///     // Since our resource may not be ready yet, the value is an Option. Our request may also fail, so the get function returns a Result
115///     // The complete type we need to match is `Option<Result<String, reqwest::Error>>`
116///     // We can use `read_unchecked` to keep our matching code in one statement while avoiding a temporary variable error (this is still completely safe because dioxus checks the borrows at runtime)
117///     match &*resource.read_unchecked() {
118///         Some(Ok(value)) => rsx! { "{value:?}" },
119///         Some(Err(err)) => rsx! { "Error: {err}" },
120///         None => rsx! { "Loading..." },
121///     }
122/// }
123/// ```
124#[derive(Debug)]
125pub struct Resource<T: 'static> {
126    waker: UseWaker<()>,
127    value: Signal<Option<T>>,
128    task: Signal<Task>,
129    state: Signal<UseResourceState>,
130    callback: Callback<(), Task>,
131}
132
133impl<T> PartialEq for Resource<T> {
134    fn eq(&self, other: &Self) -> bool {
135        self.value == other.value
136            && self.state == other.state
137            && self.task == other.task
138            && self.callback == other.callback
139    }
140}
141
142impl<T> Clone for Resource<T> {
143    fn clone(&self) -> Self {
144        *self
145    }
146}
147impl<T> Copy for Resource<T> {}
148
149/// A signal that represents the state of the resource
150// we might add more states (panicked, etc)
151#[derive(Clone, Copy, PartialEq, Hash, Eq, Debug)]
152pub enum UseResourceState {
153    /// The resource's future is still running
154    Pending,
155
156    /// The resource's future has been forcefully stopped
157    Stopped,
158
159    /// The resource's future has been paused, tempoarily
160    Paused,
161
162    /// The resource's future has completed
163    Ready,
164}
165
166impl<T> Resource<T> {
167    /// Restart the resource's future.
168    ///
169    /// This will cancel the current future and start a new one.
170    ///
171    /// ## Example
172    /// ```rust, no_run
173    /// # use dioxus::prelude::*;
174    /// # use std::time::Duration;
175    /// fn App() -> Element {
176    ///     let mut revision = use_signal(|| "1d03b42");
177    ///     let mut resource = use_resource(move || async move {
178    ///         // This will run every time the revision signal changes because we read the count inside the future
179    ///         reqwest::get(format!("https://github.com/DioxusLabs/awesome-dioxus/blob/{revision}/awesome.json")).await
180    ///     });
181    ///
182    ///     rsx! {
183    ///         button {
184    ///             // We can get a signal with the value of the resource with the `value` method
185    ///             onclick: move |_| resource.restart(),
186    ///             "Restart resource"
187    ///         }
188    ///         "{resource:?}"
189    ///     }
190    /// }
191    /// ```
192    pub fn restart(&mut self) {
193        self.task.write().cancel();
194        let new_task = self.callback.call(());
195        self.task.set(new_task);
196    }
197
198    /// Forcefully cancel the resource's future.
199    ///
200    /// ## Example
201    /// ```rust, no_run
202    /// # use dioxus::prelude::*;
203    /// # use std::time::Duration;
204    /// fn App() -> Element {
205    ///     let mut revision = use_signal(|| "1d03b42");
206    ///     let mut resource = use_resource(move || async move {
207    ///         reqwest::get(format!("https://github.com/DioxusLabs/awesome-dioxus/blob/{revision}/awesome.json")).await
208    ///     });
209    ///
210    ///     rsx! {
211    ///         button {
212    ///             // We can cancel the resource before it finishes with the `cancel` method
213    ///             onclick: move |_| resource.cancel(),
214    ///             "Cancel resource"
215    ///         }
216    ///         "{resource:?}"
217    ///     }
218    /// }
219    /// ```
220    pub fn cancel(&mut self) {
221        self.state.set(UseResourceState::Stopped);
222        self.task.write().cancel();
223    }
224
225    /// Pause the resource's future.
226    ///
227    /// ## Example
228    /// ```rust, no_run
229    /// # use dioxus::prelude::*;
230    /// # use std::time::Duration;
231    /// fn App() -> Element {
232    ///     let mut revision = use_signal(|| "1d03b42");
233    ///     let mut resource = use_resource(move || async move {
234    ///         // This will run every time the revision signal changes because we read the count inside the future
235    ///         reqwest::get(format!("https://github.com/DioxusLabs/awesome-dioxus/blob/{revision}/awesome.json")).await
236    ///     });
237    ///
238    ///     rsx! {
239    ///         button {
240    ///             // We can pause the future with the `pause` method
241    ///             onclick: move |_| resource.pause(),
242    ///             "Pause"
243    ///         }
244    ///         button {
245    ///             // And resume it with the `resume` method
246    ///             onclick: move |_| resource.resume(),
247    ///             "Resume"
248    ///         }
249    ///         "{resource:?}"
250    ///     }
251    /// }
252    /// ```
253    pub fn pause(&mut self) {
254        self.state.set(UseResourceState::Paused);
255        self.task.write().pause();
256    }
257
258    /// Resume the resource's future.
259    ///
260    /// ## Example
261    /// ```rust, no_run
262    /// # use dioxus::prelude::*;
263    /// # use std::time::Duration;
264    /// fn App() -> Element {
265    ///     let mut revision = use_signal(|| "1d03b42");
266    ///     let mut resource = use_resource(move || async move {
267    ///         // This will run every time the revision signal changes because we read the count inside the future
268    ///         reqwest::get(format!("https://github.com/DioxusLabs/awesome-dioxus/blob/{revision}/awesome.json")).await
269    ///     });
270    ///
271    ///     rsx! {
272    ///         button {
273    ///             // We can pause the future with the `pause` method
274    ///             onclick: move |_| resource.pause(),
275    ///             "Pause"
276    ///         }
277    ///         button {
278    ///             // And resume it with the `resume` method
279    ///             onclick: move |_| resource.resume(),
280    ///             "Resume"
281    ///         }
282    ///         "{resource:?}"
283    ///     }
284    /// }
285    /// ```
286    pub fn resume(&mut self) {
287        if self.finished() {
288            return;
289        }
290
291        self.state.set(UseResourceState::Pending);
292        self.task.write().resume();
293    }
294
295    /// Clear the resource's value. This will just reset the value. It will not modify any running tasks.
296    ///
297    /// ## Example
298    /// ```rust, no_run
299    /// # use dioxus::prelude::*;
300    /// # use std::time::Duration;
301    /// fn App() -> Element {
302    ///     let mut revision = use_signal(|| "1d03b42");
303    ///     let mut resource = use_resource(move || async move {
304    ///         // This will run every time the revision signal changes because we read the count inside the future
305    ///         reqwest::get(format!("https://github.com/DioxusLabs/awesome-dioxus/blob/{revision}/awesome.json")).await
306    ///     });
307    ///
308    ///     rsx! {
309    ///         button {
310    ///             // We clear the value without modifying any running tasks with the `clear` method
311    ///             onclick: move |_| resource.clear(),
312    ///             "Clear"
313    ///         }
314    ///         "{resource:?}"
315    ///     }
316    /// }
317    /// ```
318    pub fn clear(&mut self) {
319        self.value.write().take();
320    }
321
322    /// Get a handle to the inner task backing this resource
323    /// Modify the task through this handle will cause inconsistent state
324    pub fn task(&self) -> Task {
325        self.task.cloned()
326    }
327
328    /// Is the resource's future currently running?
329    pub fn pending(&self) -> bool {
330        matches!(*self.state.peek(), UseResourceState::Pending)
331    }
332
333    /// Is the resource's future currently finished running?
334    ///
335    /// Reading this does not subscribe to the future's state
336    ///
337    /// ## Example
338    /// ```rust, no_run
339    /// # use dioxus::prelude::*;
340    /// # use std::time::Duration;
341    /// fn App() -> Element {
342    ///     let mut revision = use_signal(|| "1d03b42");
343    ///     let mut resource = use_resource(move || async move {
344    ///         // This will run every time the revision signal changes because we read the count inside the future
345    ///         reqwest::get(format!("https://github.com/DioxusLabs/awesome-dioxus/blob/{revision}/awesome.json")).await
346    ///     });
347    ///
348    ///     // We can use the `finished` method to check if the future is finished
349    ///     if resource.finished() {
350    ///         rsx! {
351    ///             "The resource is finished"
352    ///         }
353    ///     } else {
354    ///         rsx! {
355    ///             "The resource is still running"
356    ///         }
357    ///     }
358    /// }
359    /// ```
360    pub fn finished(&self) -> bool {
361        matches!(
362            *self.state.peek(),
363            UseResourceState::Ready | UseResourceState::Stopped
364        )
365    }
366
367    /// Get the current state of the resource's future. This method returns a [`ReadSignal`] which can be read to get the current state of the resource or passed to other hooks and components.
368    ///
369    /// ## Example
370    /// ```rust, no_run
371    /// # use dioxus::prelude::*;
372    /// # use std::time::Duration;
373    /// fn App() -> Element {
374    ///     let mut revision = use_signal(|| "1d03b42");
375    ///     let mut resource = use_resource(move || async move {
376    ///         // This will run every time the revision signal changes because we read the count inside the future
377    ///         reqwest::get(format!("https://github.com/DioxusLabs/awesome-dioxus/blob/{revision}/awesome.json")).await
378    ///     });
379    ///
380    ///     // We can read the current state of the future with the `state` method
381    ///     match resource.state().cloned() {
382    ///         UseResourceState::Pending => rsx! {
383    ///             "The resource is still pending"
384    ///         },
385    ///         UseResourceState::Paused => rsx! {
386    ///             "The resource has been paused"
387    ///         },
388    ///         UseResourceState::Stopped => rsx! {
389    ///             "The resource has been stopped"
390    ///         },
391    ///         UseResourceState::Ready => rsx! {
392    ///             "The resource is ready!"
393    ///         },
394    ///     }
395    /// }
396    /// ```
397    pub fn state(&self) -> ReadSignal<UseResourceState> {
398        self.state.into()
399    }
400
401    /// Get the current value of the resource's future.  This method returns a [`ReadSignal`] which can be read to get the current value of the resource or passed to other hooks and components.
402    ///
403    /// ## Example
404    ///
405    /// ```rust, no_run
406    /// # use dioxus::prelude::*;
407    /// # use std::time::Duration;
408    /// fn App() -> Element {
409    ///     let mut revision = use_signal(|| "1d03b42");
410    ///     let mut resource = use_resource(move || async move {
411    ///         // This will run every time the revision signal changes because we read the count inside the future
412    ///         reqwest::get(format!("https://github.com/DioxusLabs/awesome-dioxus/blob/{revision}/awesome.json")).await
413    ///     });
414    ///
415    ///     // We can get a signal with the value of the resource with the `value` method
416    ///     let value = resource.value();
417    ///
418    ///     // Since our resource may not be ready yet, the value is an Option. Our request may also fail, so the get function returns a Result
419    ///     // The complete type we need to match is `Option<Result<String, reqwest::Error>>`
420    ///     // We can use `read_unchecked` to keep our matching code in one statement while avoiding a temporary variable error (this is still completely safe because dioxus checks the borrows at runtime)
421    ///     match &*value.read_unchecked() {
422    ///         Some(Ok(value)) => rsx! { "{value:?}" },
423    ///         Some(Err(err)) => rsx! { "Error: {err}" },
424    ///         None => rsx! { "Loading..." },
425    ///     }
426    /// }
427    /// ```
428    pub fn value(&self) -> ReadSignal<Option<T>> {
429        self.value.into()
430    }
431
432    /// Suspend the resource's future and only continue rendering when the future is ready
433    pub fn suspend(&self) -> std::result::Result<MappedSignal<T, Signal<Option<T>>>, RenderError> {
434        match self.state.cloned() {
435            UseResourceState::Stopped | UseResourceState::Paused | UseResourceState::Pending => {
436                let task = self.task();
437                if task.paused() {
438                    Ok(self.value.map(|v| v.as_ref().unwrap()))
439                } else {
440                    Err(RenderError::Suspended(SuspendedFuture::new(task)))
441                }
442            }
443            _ => Ok(self.value.map(|v| v.as_ref().unwrap())),
444        }
445    }
446}
447
448impl<T, E> Resource<Result<T, E>> {
449    /// Convert the `Resource<Result<T, E>>` into an `Option<Result<MappedSignal<T>, MappedSignal<E>>>`
450    #[allow(clippy::type_complexity)]
451    pub fn result(
452        &self,
453    ) -> Option<
454        Result<
455            MappedSignal<T, Signal<Option<Result<T, E>>>>,
456            MappedSignal<E, Signal<Option<Result<T, E>>>>,
457        >,
458    > {
459        let value: MappedSignal<T, Signal<Option<Result<T, E>>>> = self.value.map(|v| match v {
460            Some(Ok(res)) => res,
461            _ => panic!("Resource is not ready"),
462        });
463
464        let error: MappedSignal<E, Signal<Option<Result<T, E>>>> = self.value.map(|v| match v {
465            Some(Err(err)) => err,
466            _ => panic!("Resource is not ready"),
467        });
468
469        match &*self.value.peek() {
470            Some(Ok(_)) => Some(Ok(value)),
471            Some(Err(_)) => Some(Err(error)),
472            None => None,
473        }
474    }
475}
476
477impl<T> From<Resource<T>> for ReadSignal<Option<T>> {
478    fn from(val: Resource<T>) -> Self {
479        val.value.into()
480    }
481}
482
483impl<T> Readable for Resource<T> {
484    type Target = Option<T>;
485    type Storage = UnsyncStorage;
486
487    #[track_caller]
488    fn try_read_unchecked(
489        &self,
490    ) -> Result<ReadableRef<'static, Self>, generational_box::BorrowError> {
491        self.value.try_read_unchecked()
492    }
493
494    #[track_caller]
495    fn try_peek_unchecked(
496        &self,
497    ) -> Result<ReadableRef<'static, Self>, generational_box::BorrowError> {
498        self.value.try_peek_unchecked()
499    }
500
501    fn subscribers(&self) -> Subscribers {
502        self.value.subscribers()
503    }
504}
505
506impl<T> Writable for Resource<T> {
507    type WriteMetadata = <Signal<Option<T>> as Writable>::WriteMetadata;
508
509    fn try_write_unchecked(
510        &self,
511    ) -> Result<WritableRef<'static, Self>, generational_box::BorrowMutError>
512    where
513        Self::Target: 'static,
514    {
515        self.value.try_write_unchecked()
516    }
517}
518
519impl<T> IntoAttributeValue for Resource<T>
520where
521    T: Clone + IntoAttributeValue,
522{
523    fn into_value(self) -> dioxus_core::AttributeValue {
524        self.with(|f| f.clone().into_value())
525    }
526}
527
528impl<T> IntoDynNode for Resource<T>
529where
530    T: Clone + IntoDynNode,
531{
532    fn into_dyn_node(self) -> dioxus_core::DynamicNode {
533        self().into_dyn_node()
534    }
535}
536
537/// Allow calling a signal with signal() syntax
538///
539/// Currently only limited to copy types, though could probably specialize for string/arc/rc
540impl<T: Clone> Deref for Resource<T> {
541    type Target = dyn Fn() -> Option<T>;
542
543    fn deref(&self) -> &Self::Target {
544        unsafe { ReadableExt::deref_impl(self) }
545    }
546}
547
548impl<T> std::future::Future for Resource<T> {
549    type Output = ();
550
551    fn poll(
552        self: std::pin::Pin<&mut Self>,
553        cx: &mut std::task::Context<'_>,
554    ) -> std::task::Poll<Self::Output> {
555        match self.waker.clone().poll_unpin(cx) {
556            std::task::Poll::Ready(_) => std::task::Poll::Ready(()),
557            std::task::Poll::Pending => std::task::Poll::Pending,
558        }
559    }
560}