1
0

use_resource.rs 16 KB

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