dioxus_hooks/
use_resource.rs

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