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}