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