dioxus_hooks/
use_resource.rs

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