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::ops::Deref;
15use std::{cell::Cell, future::Future, rc::Rc};
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 finished running?
328    ///
329    /// Reading this does not subscribe to the future's state
330    ///
331    /// ## Example
332    /// ```rust, no_run
333    /// # use dioxus::prelude::*;
334    /// # use std::time::Duration;
335    /// fn App() -> Element {
336    ///     let mut revision = use_signal(|| "1d03b42");
337    ///     let mut resource = use_resource(move || async move {
338    ///         // This will run every time the revision signal changes because we read the count inside the future
339    ///         reqwest::get(format!("https://github.com/DioxusLabs/awesome-dioxus/blob/{revision}/awesome.json")).await
340    ///     });
341    ///
342    ///     // We can use the `finished` method to check if the future is finished
343    ///     if resource.finished() {
344    ///         rsx! {
345    ///             "The resource is finished"
346    ///         }
347    ///     } else {
348    ///         rsx! {
349    ///             "The resource is still running"
350    ///         }
351    ///     }
352    /// }
353    /// ```
354    pub fn finished(&self) -> bool {
355        matches!(
356            *self.state.peek(),
357            UseResourceState::Ready | UseResourceState::Stopped
358        )
359    }
360
361    /// 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.
362    ///
363    /// ## Example
364    /// ```rust, no_run
365    /// # use dioxus::prelude::*;
366    /// # use std::time::Duration;
367    /// fn App() -> Element {
368    ///     let mut revision = use_signal(|| "1d03b42");
369    ///     let mut resource = use_resource(move || async move {
370    ///         // This will run every time the revision signal changes because we read the count inside the future
371    ///         reqwest::get(format!("https://github.com/DioxusLabs/awesome-dioxus/blob/{revision}/awesome.json")).await
372    ///     });
373    ///
374    ///     // We can read the current state of the future with the `state` method
375    ///     match resource.state().cloned() {
376    ///         UseResourceState::Pending => rsx! {
377    ///             "The resource is still pending"
378    ///         },
379    ///         UseResourceState::Paused => rsx! {
380    ///             "The resource has been paused"
381    ///         },
382    ///         UseResourceState::Stopped => rsx! {
383    ///             "The resource has been stopped"
384    ///         },
385    ///         UseResourceState::Ready => rsx! {
386    ///             "The resource is ready!"
387    ///         },
388    ///     }
389    /// }
390    /// ```
391    pub fn state(&self) -> ReadSignal<UseResourceState> {
392        self.state.into()
393    }
394
395    /// 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.
396    ///
397    /// ## Example
398    ///
399    /// ```rust, no_run
400    /// # use dioxus::prelude::*;
401    /// # use std::time::Duration;
402    /// fn App() -> Element {
403    ///     let mut revision = use_signal(|| "1d03b42");
404    ///     let mut resource = use_resource(move || async move {
405    ///         // This will run every time the revision signal changes because we read the count inside the future
406    ///         reqwest::get(format!("https://github.com/DioxusLabs/awesome-dioxus/blob/{revision}/awesome.json")).await
407    ///     });
408    ///
409    ///     // We can get a signal with the value of the resource with the `value` method
410    ///     let value = resource.value();
411    ///
412    ///     // 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
413    ///     // The complete type we need to match is `Option<Result<String, reqwest::Error>>`
414    ///     // 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)
415    ///     match &*value.read_unchecked() {
416    ///         Some(Ok(value)) => rsx! { "{value:?}" },
417    ///         Some(Err(err)) => rsx! { "Error: {err}" },
418    ///         None => rsx! { "Loading..." },
419    ///     }
420    /// }
421    /// ```
422    pub fn value(&self) -> ReadSignal<Option<T>> {
423        self.value.into()
424    }
425
426    /// Suspend the resource's future and only continue rendering when the future is ready
427    pub fn suspend(&self) -> std::result::Result<MappedSignal<T, Signal<Option<T>>>, RenderError> {
428        match self.state.cloned() {
429            UseResourceState::Stopped | UseResourceState::Paused | UseResourceState::Pending => {
430                let task = self.task();
431                if task.paused() {
432                    Ok(self.value.map(|v| v.as_ref().unwrap()))
433                } else {
434                    Err(RenderError::Suspended(SuspendedFuture::new(task)))
435                }
436            }
437            _ => Ok(self.value.map(|v| v.as_ref().unwrap())),
438        }
439    }
440}
441
442impl<T> From<Resource<T>> for ReadSignal<Option<T>> {
443    fn from(val: Resource<T>) -> Self {
444        val.value.into()
445    }
446}
447
448impl<T> Readable for Resource<T> {
449    type Target = Option<T>;
450    type Storage = UnsyncStorage;
451
452    #[track_caller]
453    fn try_read_unchecked(
454        &self,
455    ) -> Result<ReadableRef<'static, Self>, generational_box::BorrowError> {
456        self.value.try_read_unchecked()
457    }
458
459    #[track_caller]
460    fn try_peek_unchecked(
461        &self,
462    ) -> Result<ReadableRef<'static, Self>, generational_box::BorrowError> {
463        self.value.try_peek_unchecked()
464    }
465
466    fn subscribers(&self) -> Subscribers {
467        self.value.subscribers()
468    }
469}
470
471impl<T> IntoAttributeValue for Resource<T>
472where
473    T: Clone + IntoAttributeValue,
474{
475    fn into_value(self) -> dioxus_core::AttributeValue {
476        self.with(|f| f.clone().into_value())
477    }
478}
479
480impl<T> IntoDynNode for Resource<T>
481where
482    T: Clone + IntoDynNode,
483{
484    fn into_dyn_node(self) -> dioxus_core::DynamicNode {
485        self().into_dyn_node()
486    }
487}
488
489/// Allow calling a signal with signal() syntax
490///
491/// Currently only limited to copy types, though could probably specialize for string/arc/rc
492impl<T: Clone> Deref for Resource<T> {
493    type Target = dyn Fn() -> Option<T>;
494
495    fn deref(&self) -> &Self::Target {
496        unsafe { ReadableExt::deref_impl(self) }
497    }
498}
499
500impl<T> std::future::Future for Resource<T> {
501    type Output = ();
502
503    fn poll(
504        self: std::pin::Pin<&mut Self>,
505        cx: &mut std::task::Context<'_>,
506    ) -> std::task::Poll<Self::Output> {
507        match self.waker.clone().poll_unpin(cx) {
508            std::task::Poll::Ready(_) => std::task::Poll::Ready(()),
509            std::task::Poll::Pending => std::task::Poll::Pending,
510        }
511    }
512}