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}