wasm_react/hooks/
use_effect.rs

1use super::{use_ref, Deps};
2use crate::react_bindings;
3use wasm_bindgen::{prelude::Closure, JsValue, UnwrapThrowExt};
4
5/// Denotes types that can be used as destructors for effects.
6pub trait IntoDestructor {
7  #[doc(hidden)]
8  type Destructor: FnOnce() + 'static;
9
10  #[doc(hidden)]
11  fn into_destructor(self) -> Self::Destructor;
12}
13
14impl IntoDestructor for () {
15  type Destructor = fn();
16
17  fn into_destructor(self) -> Self::Destructor {
18    || ()
19  }
20}
21
22impl<F> IntoDestructor for F
23where
24  F: FnOnce() + 'static,
25{
26  type Destructor = F;
27
28  fn into_destructor(self) -> Self::Destructor {
29    self
30  }
31}
32
33fn use_effect_inner<G, D>(
34  effect: impl FnOnce() -> G + 'static,
35  deps: Deps<D>,
36  f: impl FnOnce(&JsValue, u8),
37) where
38  G: IntoDestructor,
39  D: PartialEq + 'static,
40{
41  let create_effect_closure = move || {
42    Closure::once(move || {
43      let destructor = effect();
44
45      // The effect destructor will definitely be called exactly once by React
46      Closure::once_into_js(destructor.into_destructor())
47    })
48  };
49
50  let mut ref_container =
51    use_ref(None::<(Closure<dyn FnMut() -> JsValue>, Deps<D>, u8)>);
52
53  let new_value = match ref_container.current_mut().take() {
54    Some((old_effect, old_deps, counter)) => {
55      if deps.is_all() || old_deps != deps {
56        Some((create_effect_closure(), deps, counter.wrapping_add(1)))
57      } else {
58        // Dependencies didn't change
59        Some((old_effect, old_deps, counter))
60      }
61    }
62    None => Some((create_effect_closure(), deps, 0)),
63  };
64
65  ref_container.set_current(new_value);
66
67  let value = ref_container.current();
68  let (effect, _, counter) =
69    value.as_ref().expect_throw("no effect data available");
70
71  f(effect.as_ref(), *counter);
72}
73
74/// Runs a function which contains imperative code that may cause side-effects.
75///
76/// The given function will run after render is committed to the screen when
77/// the given dependencies have changed from last render. The function can
78/// return a clean-up function.
79///
80/// # Example
81///
82/// ```
83/// # use wasm_react::{*, hooks::*};
84/// #
85/// # fn fetch(url: &str) -> String { String::new() }
86/// # struct C { url: &'static str }
87/// # impl C {
88/// #   fn f(&self) {
89/// let state = use_state(|| None);
90///
91/// use_effect({
92///   clones!(self.url, mut state);
93///
94///   move || {
95///     state.set(|_| Some(fetch(url)));
96///   }
97/// }, Deps::some(self.url));
98/// #
99/// #   }
100/// # }
101/// ```
102pub fn use_effect<G, D>(effect: impl FnOnce() -> G + 'static, deps: Deps<D>)
103where
104  G: IntoDestructor,
105  D: PartialEq + 'static,
106{
107  use_effect_inner(effect, deps, react_bindings::use_rust_effect);
108}
109
110/// Same as [`use_effect()`], but it fires synchronously after all DOM mutations.
111///
112/// See [React documentation](https://react.dev/reference/react/useLayoutEffect).
113pub fn use_layout_effect<G, D>(
114  effect: impl FnOnce() -> G + 'static,
115  deps: Deps<D>,
116) where
117  G: IntoDestructor,
118  D: PartialEq + 'static,
119{
120  use_effect_inner(effect, deps, react_bindings::use_rust_layout_effect);
121}