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}