dioxus_hooks/
use_resource.rs

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