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}