yew_api_hook/
hooks.rs

1#[cfg(feature = "cache")]
2use crate::CachableRequest;
3use crate::Request;
4
5use yew::prelude::*;
6use yew::suspense::SuspensionResult;
7#[cfg(feature = "cache")]
8use yewdux::prelude::use_store_value;
9
10/// Use API Options
11///
12/// You may specify dependencies which force the request to be reevaluated
13/// and a handler which is called every time a request is ran
14#[derive(Clone, Debug)]
15pub struct Options<R, D>
16where
17    R: Request + 'static,
18    D: Clone + PartialEq + 'static,
19{
20    pub deps: Option<D>,
21    pub handler: Option<Callback<Result<R::Output, R::Error>, ()>>,
22}
23
24impl<R, D> Default for Options<R, D>
25where
26    R: Request + 'static,
27    D: Clone + PartialEq + 'static,
28{
29    fn default() -> Self {
30        Self {
31            deps: None,
32            handler: None,
33        }
34    }
35}
36
37/// The basic api hook which requests data on mount and preserves its
38/// data through out the component lifetime
39#[hook]
40pub fn use_api<R: Request + 'static>(request: R) -> SuspensionResult<Result<R::Output, R::Error>> {
41    use_api_with_options::<R, ()>(request, Default::default())
42}
43
44/// The basic api hook which requests data on mount and preserves its
45/// data through out the component lifetime.
46///
47/// Reruns the request once the dependencies update
48#[hook]
49pub fn use_api_with_options<R: Request + 'static, D: Clone + PartialEq + 'static>(
50    request: R,
51    options: Options<R, D>,
52) -> SuspensionResult<Result<R::Output, R::Error>> {
53    let deps = (request, options.deps);
54
55    let result = inner::use_future_with_deps(
56        |deps| async move {
57            let result = deps.0.run().await;
58
59            if let Some(ref handler) = options.handler {
60                handler.emit(result.to_owned());
61            }
62
63            if let Ok(ref data) = result {
64                R::store(data.to_owned());
65            }
66
67            result
68        },
69        deps,
70    )?;
71
72    Ok((*result).to_owned())
73}
74
75/// A lazy api response which you can trigger through the `run` callback
76pub struct LazyResponse<R: Request + 'static> {
77    pub run: Callback<(), ()>,
78    pub data: Option<SuspensionResult<Result<R::Output, R::Error>>>,
79}
80
81/// Useful when not wanting to run a request on mount, e.g. for a logout button
82/// You may run the request multiple times through multiple emits of the callback
83#[hook]
84pub fn use_api_lazy<R: Request + 'static>(request: R) -> LazyResponse<R> {
85    use_api_lazy_with_options::<R, ()>(request, Default::default())
86}
87
88/// Useful when not wanting to run a request on mount, e.g. for a logout button
89/// You may run the request multiple times through multiple emits of the callback
90#[hook]
91pub fn use_api_lazy_with_options<R: Request + 'static, D: Clone + PartialEq + 'static>(
92    request: R,
93    options: Options<R, D>,
94) -> LazyResponse<R> {
95    let DynLazyResponse { run, data } = use_api_dynamic_with_options::<R, D>(options);
96
97    let run = Callback::from(move |_| {
98        run.emit(request.clone());
99    });
100
101    LazyResponse { run, data }
102}
103
104pub struct DynLazyResponse<R: Request + 'static> {
105    pub run: Callback<R, ()>,
106    pub data: Option<SuspensionResult<Result<R::Output, R::Error>>>,
107}
108
109/// Useful when not wanting to run a request on mount, e.g. for a logout button
110/// You may run the request multiple times through multiple emits of the callback
111///
112/// By using the dynamic hook you can build the request with its parameters at runtime
113#[hook]
114pub fn use_api_dynamic<R: Request + 'static>() -> DynLazyResponse<R> {
115    use_api_dynamic_with_options::<R, ()>(Default::default())
116}
117
118/// Useful when not wanting to run a request on mount, e.g. for a logout button
119/// You may run the request multiple times through multiple emits of the callback
120///
121/// By using the dynamic hook you can build the request with its parameters at runtime
122#[hook]
123pub fn use_api_dynamic_with_options<R: Request + 'static, D: Clone + PartialEq + 'static>(
124    options: Options<R, D>,
125) -> DynLazyResponse<R> {
126    let request = use_state(|| Option::<R>::None);
127
128    let deps = ((*request).clone(), options.deps);
129
130    let (run, result) = inner::use_future_callback(
131        |request| async move {
132            let Some(ref request) = request.0 else {
133                return None;
134            };
135
136            let result = request.run().await;
137
138            if let Some(ref handler) = options.handler {
139                handler.emit(result.to_owned());
140            }
141
142            if let Ok(ref data) = result {
143                R::store(data.to_owned());
144            }
145
146            Some(result)
147        },
148        deps,
149    );
150
151    let run = Callback::from(move |r| {
152        request.set(Some(r));
153        run.emit(());
154    });
155
156    if let Some(Ok(false)) = result.as_ref().map(|o| o.as_ref().map(|sr| sr.is_some())) {
157        return DynLazyResponse { run, data: None };
158    }
159
160    let data = result.map(|res| res.map(|res| (*res).clone().unwrap()));
161
162    DynLazyResponse { run, data }
163}
164
165/// Use the locally cached data instead of running the api request if possible
166#[cfg(feature = "cache")]
167#[hook]
168pub fn use_cachable_api<R: Request + CachableRequest + 'static>(
169    request: R,
170) -> SuspensionResult<Result<R::Output, R::Error>> {
171    use_cachable_api_with_options::<R, ()>(request, Default::default())
172}
173
174/// Use the locally cached data instead of running the api request if possible
175#[cfg(feature = "cache")]
176#[hook]
177pub fn use_cachable_api_with_options<
178    R: Request + CachableRequest + 'static,
179    D: Clone + PartialEq + 'static,
180>(
181    request: R,
182    options: Options<R, D>,
183) -> SuspensionResult<Result<R::Output, R::Error>> {
184    let store = use_store_value::<R::Store>();
185    let deps = (request, options.deps);
186    let result = inner::use_future_with_deps(
187        |deps| async move {
188            if let Some(cache) = deps.0.load(store) {
189                return Ok(cache);
190            }
191
192            let result = deps.0.run().await;
193
194            if let Some(ref handler) = options.handler {
195                handler.emit(result.to_owned());
196            }
197
198            if let Ok(ref data) = result {
199                R::store(data.to_owned());
200            }
201
202            result
203        },
204        deps,
205    )?;
206
207    Ok((*result).to_owned())
208}
209
210/// Use the locally cached data instead of running the api request if possible
211/// Only returns a result once the callback was emitted
212#[cfg(feature = "cache")]
213#[hook]
214pub fn use_cachable_api_lazy<R: Request + CachableRequest + 'static>(
215    request: R,
216) -> LazyResponse<R> {
217    use_cachable_api_lazy_with_options::<R, ()>(request, Default::default())
218}
219
220/// Use the locally cached data instead of running the api request if possible
221/// Only returns a result once the callback was emitted
222#[cfg(feature = "cache")]
223#[hook]
224pub fn use_cachable_api_lazy_with_options<
225    R: Request + CachableRequest + 'static,
226    D: Clone + PartialEq + 'static,
227>(
228    request: R,
229    options: Options<R, D>,
230) -> LazyResponse<R> {
231    let DynLazyResponse { run, data } = use_cachable_api_dynamic_with_options::<R, D>(options);
232
233    let run = Callback::from(move |_| {
234        run.emit(request.clone());
235    });
236
237    LazyResponse { run, data }
238}
239
240#[cfg(feature = "cache")]
241#[hook]
242pub fn use_cachable_api_dynamic<R: Request + CachableRequest + 'static>() -> DynLazyResponse<R> {
243    use_cachable_api_dynamic_with_options::<R, ()>(Default::default())
244}
245
246/// Useful when not wanting to run a request on mount, e.g. for a logout button
247/// You may run the request multiple times through multiple emits of the callback
248///
249/// By using the dynamic hook you can build the request with its parameters at runtime
250#[cfg(feature = "cache")]
251#[hook]
252pub fn use_cachable_api_dynamic_with_options<
253    R: Request + CachableRequest + 'static,
254    D: Clone + PartialEq + 'static,
255>(
256    options: Options<R, D>,
257) -> DynLazyResponse<R> {
258    let store = use_store_value::<R::Store>();
259    let request = use_state(|| Option::<R>::None);
260
261    let deps = (request.clone(), options.deps);
262
263    let (run, result) = inner::use_future_callback(
264        |deps| async move {
265            let Some(ref request) = *(deps.0) else {
266                return None;
267            };
268
269            if let Some(cache) = request.load(store) {
270                return Some(Ok(cache));
271            }
272
273            let result = request.run().await;
274
275            if let Some(ref handler) = options.handler {
276                handler.emit(result.to_owned());
277            }
278
279            if let Ok(ref data) = result {
280                R::store(data.to_owned());
281            }
282
283            Some(result)
284        },
285        deps,
286    );
287
288    let run = Callback::from(move |r| {
289        request.set(Some(r));
290        run.emit(());
291    });
292
293    if let Some(Ok(false)) = result.as_ref().map(|o| o.as_ref().map(|sr| sr.is_some())) {
294        return DynLazyResponse { run, data: None };
295    }
296
297    let data = result.map(|res| res.map(|res| (*res).clone().unwrap()));
298
299    DynLazyResponse { run, data }
300}
301
302/// from yew@next
303mod inner {
304    use std::borrow::Borrow;
305    use std::cell::Cell;
306    use std::fmt;
307    use std::future::Future;
308    use std::ops::Deref;
309    use std::rc::Rc;
310
311    use yew::prelude::*;
312    use yew::suspense::{Suspension, SuspensionResult};
313
314    pub struct UseFutureHandle<O> {
315        inner: UseStateHandle<Option<O>>,
316    }
317
318    impl<O> Deref for UseFutureHandle<O> {
319        type Target = O;
320
321        fn deref(&self) -> &Self::Target {
322            self.inner.as_ref().unwrap()
323        }
324    }
325
326    impl<T: fmt::Debug> fmt::Debug for UseFutureHandle<T> {
327        fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
328            f.debug_struct("UseFutureHandle")
329                .field("value", &format!("{:?}", self.inner))
330                .finish()
331        }
332    }
333
334    #[hook]
335    pub fn use_future<F, T, O>(init_f: F) -> SuspensionResult<UseFutureHandle<O>>
336    where
337        F: FnOnce() -> T,
338        T: Future<Output = O> + 'static,
339        O: 'static,
340    {
341        use_future_with_deps(move |_| init_f(), ())
342    }
343
344    #[hook]
345    pub fn use_future_with_deps<F, D, T, O>(f: F, deps: D) -> SuspensionResult<UseFutureHandle<O>>
346    where
347        F: FnOnce(Rc<D>) -> T,
348        T: Future<Output = O> + 'static,
349        O: 'static,
350        D: PartialEq + 'static,
351    {
352        let output = use_state(|| None);
353        // We only commit a result if it comes from the latest spawned future. Otherwise, this
354        // might trigger pointless updates or even override newer state.
355        let latest_id = use_state(|| Cell::new(0u32));
356
357        let suspension = {
358            let output = output.clone();
359
360            use_memo_base(
361                move |deps| {
362                    let self_id = latest_id.get().wrapping_add(1);
363                    // As long as less than 2**32 futures are in flight wrapping_add is fine
364                    (*latest_id).set(self_id);
365                    let deps = Rc::new(deps);
366                    let task = f(deps.clone());
367                    let suspension = Suspension::from_future(async move {
368                        let result = task.await;
369                        if latest_id.get() == self_id {
370                            output.set(Some(result));
371                        }
372                    });
373                    (suspension, deps)
374                },
375                deps,
376            )
377        };
378
379        if suspension.resumed() {
380            Ok(UseFutureHandle { inner: output })
381        } else {
382            Err((*suspension).clone())
383        }
384    }
385
386    #[hook]
387    pub fn use_future_callback<F, D, T, O>(
388        f: F,
389        deps: D,
390    ) -> (
391        Callback<(), ()>,
392        Option<SuspensionResult<UseFutureHandle<O>>>,
393    )
394    where
395        F: FnOnce(Rc<D>) -> T,
396        T: Future<Output = O> + 'static,
397        O: 'static,
398        D: Clone + PartialEq + 'static,
399    {
400        let execution = use_state(|| false);
401        let execute: Callback<(), ()> = {
402            let execution = execution.clone();
403            use_callback(move |_, _| execution.set(true), ())
404        };
405
406        let output = use_state(|| None);
407        // We only commit a result if it comes from the latest spawned future. Otherwise, this
408        // might trigger pointless updates or even override newer state.
409        let latest_id = use_state(|| Cell::new(0u32));
410
411        let suspension = {
412            let output = output.clone();
413
414            let deps = (deps, execution.clone());
415            use_memo_base(
416                move |deps| {
417                    if !(*execution) {
418                        return (None, deps);
419                    }
420
421                    let self_id = latest_id.get().wrapping_add(1);
422                    // As long as less than 2**32 futures are in flight wrapping_add is fine
423                    (*latest_id).set(self_id);
424                    let task = f(Rc::new(deps.0.clone()));
425                    let suspension = Suspension::from_future(async move {
426                        let result = task.await;
427
428                        if latest_id.get() == self_id {
429                            output.set(Some(result));
430                        }
431                        execution.set(false);
432                    });
433                    (Some(suspension), (deps.0.to_owned(), deps.1))
434                },
435                deps,
436            )
437        };
438
439        if let Some(ref suspension) = *suspension {
440            if suspension.resumed() {
441                return (execute, Some(Ok(UseFutureHandle { inner: output })));
442            } else {
443                return (execute, Some(Err(suspension.clone())));
444            }
445        }
446
447        if output.is_some() {
448            return (execute, Some(Ok(UseFutureHandle { inner: output })));
449        }
450
451        (execute, None)
452    }
453
454    #[hook]
455    pub(crate) fn use_memo_base<T, F, D, K>(f: F, deps: D) -> Rc<T>
456    where
457        T: 'static,
458        F: FnOnce(D) -> (T, K),
459        K: 'static + Borrow<D>,
460        D: PartialEq,
461    {
462        struct MemoState<T, K> {
463            memo_key: K,
464            result: Rc<T>,
465        }
466        let state = use_mut_ref(|| -> Option<MemoState<T, K>> { None });
467
468        let mut state = state.borrow_mut();
469        match &*state {
470            Some(existing) if existing.memo_key.borrow() != &deps => {
471                // Drop old state if it's outdated
472                *state = None;
473            }
474            _ => {}
475        };
476        let state = state.get_or_insert_with(|| {
477            let (result, memo_key) = f(deps);
478            let result = Rc::new(result);
479            MemoState { result, memo_key }
480        });
481        state.result.clone()
482    }
483}