yew_hooks/hooks/use_async.rs
1use std::ops::Deref;
2use std::{future::Future, rc::Rc};
3
4use wasm_bindgen_futures::spawn_local;
5use yew::prelude::*;
6
7use super::{use_mount, use_mut_latest};
8
9/// Options for [`use_async_with_options`].
10#[derive(Default)]
11pub struct UseAsyncOptions {
12 pub auto: bool,
13}
14
15impl UseAsyncOptions {
16 /// Automatically run when mount
17 pub const fn enable_auto() -> Self {
18 Self { auto: true }
19 }
20}
21
22/// State for an async future.
23#[derive(PartialEq, Eq)]
24pub struct UseAsyncState<T, E> {
25 pub loading: bool,
26 pub data: Option<T>,
27 pub error: Option<E>,
28}
29
30/// State handle for the [`use_async`] hook.
31pub struct UseAsyncHandle<T, E> {
32 inner: UseStateHandle<UseAsyncState<T, E>>,
33 run: Rc<dyn Fn()>,
34}
35
36impl<T, E> UseAsyncHandle<T, E> {
37 /// Start to resolve the async future to a final value.
38 pub fn run(&self) {
39 (self.run)();
40 }
41
42 /// Update `data` directly.
43 pub fn update(&self, data: T) {
44 self.inner.set(UseAsyncState {
45 loading: false,
46 data: Some(data),
47 error: None,
48 });
49 }
50}
51
52impl<T, E> Deref for UseAsyncHandle<T, E> {
53 type Target = UseAsyncState<T, E>;
54
55 fn deref(&self) -> &Self::Target {
56 &self.inner
57 }
58}
59
60impl<T, E> Clone for UseAsyncHandle<T, E> {
61 fn clone(&self) -> Self {
62 Self {
63 inner: self.inner.clone(),
64 run: self.run.clone(),
65 }
66 }
67}
68
69impl<T, E> PartialEq for UseAsyncHandle<T, E>
70where
71 T: PartialEq,
72 E: PartialEq,
73{
74 fn eq(&self, other: &Self) -> bool {
75 *self.inner == *other.inner
76 }
77}
78
79/// This hook returns state and a `run` callback for an async future.
80///
81/// # Example
82///
83/// ```rust
84/// # use yew::prelude::*;
85/// #
86/// use yew_hooks::prelude::*;
87///
88/// #[function_component(Async)]
89/// fn async_test() -> Html {
90/// let state = use_async(async move {
91/// fetch("/api/user/123".to_string()).await
92/// });
93///
94/// let onclick = {
95/// let state = state.clone();
96/// Callback::from(move |_| {
97/// state.run();
98/// })
99/// };
100///
101/// html! {
102/// <div>
103/// <button {onclick} disabled={state.loading}>{ "Start loading" }</button>
104/// {
105/// if state.loading {
106/// html! { "Loading" }
107/// } else {
108/// html! {}
109/// }
110/// }
111/// {
112/// if let Some(data) = &state.data {
113/// html! { data }
114/// } else {
115/// html! {}
116/// }
117/// }
118/// {
119/// if let Some(error) = &state.error {
120/// html! { error }
121/// } else {
122/// html! {}
123/// }
124/// }
125/// </div>
126/// }
127/// }
128///
129/// async fn fetch(url: String) -> Result<String, String> {
130/// // You can use reqwest to fetch your http api
131/// Ok(String::from("Jet Li"))
132/// }
133/// ```
134#[hook]
135pub fn use_async<F, T, E>(future: F) -> UseAsyncHandle<T, E>
136where
137 F: Future<Output = Result<T, E>> + 'static,
138 T: Clone + 'static,
139 E: Clone + 'static,
140{
141 use_async_with_options(future, UseAsyncOptions::default())
142}
143
144/// This hook returns state and a `run` callback for an async future with options.
145/// See [`use_async`] too.
146///
147/// # Example
148///
149/// ```rust
150/// # use yew::prelude::*;
151/// #
152/// use yew_hooks::prelude::*;
153///
154/// #[function_component(Async)]
155/// fn async_test() -> Html {
156/// let state = use_async_with_options(async move {
157/// fetch("/api/user/123".to_string()).await
158/// }, UseAsyncOptions::enable_auto());
159///
160/// html! {
161/// <div>
162/// {
163/// if state.loading {
164/// html! { "Loading" }
165/// } else {
166/// html! {}
167/// }
168/// }
169/// {
170/// if let Some(data) = &state.data {
171/// html! { data }
172/// } else {
173/// html! {}
174/// }
175/// }
176/// {
177/// if let Some(error) = &state.error {
178/// html! { error }
179/// } else {
180/// html! {}
181/// }
182/// }
183/// </div>
184/// }
185/// }
186///
187/// async fn fetch(url: String) -> Result<String, String> {
188/// // You can use reqwest to fetch your http api
189/// Ok(String::from("Jet Li"))
190/// }
191/// ```
192#[hook]
193pub fn use_async_with_options<F, T, E>(future: F, options: UseAsyncOptions) -> UseAsyncHandle<T, E>
194where
195 F: Future<Output = Result<T, E>> + 'static,
196 T: Clone + 'static,
197 E: Clone + 'static,
198{
199 let inner = use_state(|| UseAsyncState {
200 loading: false,
201 data: None,
202 error: None,
203 });
204 let future_ref = use_mut_latest(Some(future));
205
206 let run = {
207 let inner = inner.clone();
208 Rc::new(move || {
209 let inner = inner.clone();
210 let future_ref = future_ref.clone();
211 spawn_local(async move {
212 let future_ref = future_ref.current();
213 let future = (*future_ref.borrow_mut()).take();
214
215 if let Some(future) = future {
216 // Only set loading to true and leave previous data/error alone.
217 inner.set(UseAsyncState {
218 loading: true,
219 data: inner.data.clone(),
220 error: inner.error.clone(),
221 });
222 match future.await {
223 // Success with some data and clear previous error.
224 Ok(data) => inner.set(UseAsyncState {
225 loading: false,
226 data: Some(data),
227 error: None,
228 }),
229 // Failed with some error and leave previous data alone.
230 Err(error) => inner.set(UseAsyncState {
231 loading: false,
232 data: inner.data.clone(),
233 error: Some(error),
234 }),
235 }
236 }
237 });
238 })
239 };
240
241 {
242 let run = run.clone();
243 use_mount(move || {
244 if options.auto {
245 run();
246 }
247 });
248 }
249
250 UseAsyncHandle { inner, run }
251}