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}