hooks/effect/
hook.rs

1use std::{marker::PhantomData, task::Poll};
2
3use super::{inner::EffectInner, EffectCleanup, EffectFor};
4
5pub struct Effect<Dep, E: EffectFor<Dep>> {
6    dependency: Option<Dep>,
7    inner: EffectInner<E, E::Cleanup>,
8}
9
10impl<Dep, E: EffectFor<Dep>> Unpin for Effect<Dep, E> {}
11
12impl<Dep, E: EffectFor<Dep>> Default for Effect<Dep, E> {
13    fn default() -> Self {
14        Self {
15            dependency: None,
16            inner: Default::default(),
17        }
18    }
19}
20
21impl<Dep: std::fmt::Debug, E: EffectFor<Dep>> std::fmt::Debug for Effect<Dep, E> {
22    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
23        f.debug_struct("Effect")
24            .field("dependency", &self.dependency)
25            .field("inner", &self.inner)
26            .finish()
27    }
28}
29
30hooks_core::impl_hook![
31    impl<Dep, E: EffectFor<Dep>> Effect<Dep, E> {
32        #[inline]
33        fn unmount(self) {
34            self.get_mut().inner.unmount()
35        }
36        fn poll_next_update(self) {
37            let this = self.get_mut();
38            if this.inner.effect.is_some() {
39                if let Some(dependency) = &this.dependency {
40                    this.inner.cleanup_and_effect_for(dependency);
41                }
42            }
43            Poll::Ready(false)
44        }
45        #[inline(always)]
46        fn use_hook(self) {}
47    }
48];
49
50impl<Dep, E: EffectFor<Dep>> Effect<Dep, E> {
51    pub fn register_effect_if_dep_ne(self: std::pin::Pin<&mut Self>, effect: E, dep: Dep)
52    where
53        Dep: PartialEq,
54    {
55        let this = self.get_mut();
56
57        let dep = Some(dep);
58        if this.dependency != dep {
59            this.dependency = dep;
60            this.inner.register_effect(effect)
61        }
62    }
63
64    pub fn register_effect_if(
65        self: std::pin::Pin<&mut Self>,
66        get_new_dep_and_effect: impl FnOnce(&mut Option<Dep>) -> Option<E>,
67    ) {
68        let this = self.get_mut();
69        if let Some(new_effect) = get_new_dep_and_effect(&mut this.dependency) {
70            this.inner.register_effect(new_effect)
71        }
72    }
73}
74
75/// Register an effect for a dependency.
76/// The effect will be run in the [`poll_next_update`]
77/// after a new effect is registered.
78///
79/// [`use_hook`]: crate::Hook::use_hook
80/// [`poll_next_update`]: crate::HookPollNextUpdate::poll_next_update
81///
82/// ## Usage
83///
84/// You can pass an effect with an dependency which impl [`PartialEq`].
85/// If the dependency changes (`dependency != old_dependency`),
86/// `effect` will be registered and run in the further [`poll_next_update`].
87///
88/// ```
89/// # use hooks::{hook_fn, use_effect}; fn do_some_effects() {} hook_fn!( fn use_demo() {
90/// # let effect = |_: &_| {}; let dependency = ();
91/// use_effect(effect, dependency);
92/// # });
93/// ```
94///
95/// ```
96/// # use hooks::{hook_fn, use_effect}; fn do_some_effects() {} hook_fn!( fn use_demo() {
97/// use_effect(|dep: &i32| {
98///     do_some_effects();
99/// }, 0);
100/// # });
101/// ```
102///
103/// `effect` can return a cleanup (which impl [`EffectCleanup`]).
104/// When the effect is run, the returned cleanup will be registered.
105/// If further [`use_hook`] registers new dependency and new effect,
106/// in the further [`poll_next_update`],
107/// the cleanup will be run and then the new effect will be run.
108/// When the hook is [`unmount`](crate::HookUnmount::unmount)ed or dropped, the last cleanup will be run.
109///
110/// ```
111/// # use hooks::{hook_fn, use_effect}; fn do_some_effects() {} fn do_some_cleanup() {} hook_fn!( fn use_demo() {
112/// use_effect(|dep: &i32| {
113///     do_some_effects();
114///     || do_some_cleanup()
115/// }, 0);
116/// # });
117/// ```
118///
119/// `use_effect(effect, dependency)` requires you to move the dependency
120/// even it may be equal to the old dependency.
121/// You can conditionally register new dependency and new effect with [`use_effect_with`].
122///
123/// ## Examples
124///
125/// ```
126/// # use hooks::prelude::*;
127/// hook_fn!(
128///     fn use_print_effect() {
129///         h![use_effect(|_: &_| {
130///             println!("do some effects");
131///
132///             // Return an optional cleanup function
133///             move || println!("cleaning up")
134///         }, ())]
135///     }
136/// );
137///
138/// # futures_lite::future::block_on(async {
139/// let mut hook = use_print_effect().into_hook();
140///
141/// println!("hook created");
142///
143/// assert!(hook.next_value().await.is_some());
144///
145/// println!("first next_value returned");
146///
147/// assert!(hook.next_value().await.is_none());
148///
149/// println!("second next_value returned");
150///
151/// drop(hook);
152///
153/// println!("hook is dropped");
154/// # });
155/// ```
156///
157/// The above code would print:
158///
159/// ```txt
160/// hook created
161/// first next_value returned
162/// do some effects
163/// second next_value returned
164/// cleaning up
165/// hook is dropped
166/// ```
167pub struct UseEffect<Dep: PartialEq, E: EffectFor<Dep>>(pub E, pub Dep);
168pub use UseEffect as use_effect;
169
170hooks_core::impl_hook![
171    impl<Dep: PartialEq, E: EffectFor<Dep>> UseEffect<Dep, E> {
172        #[inline]
173        fn into_hook(self) -> Effect<Dep, E> {
174            Effect {
175                dependency: Some(self.1),
176                inner: EffectInner::new_registered(self.0),
177            }
178        }
179        #[inline]
180        fn update_hook(self, hook: _) {
181            hook.register_effect_if_dep_ne(self.0, self.1)
182        }
183        #[inline]
184        fn h(self, hook: Effect<Dep, E>) {
185            hook.register_effect_if_dep_ne(self.0, self.1)
186        }
187    }
188];
189
190pub struct UseEffectWith<Dep, E: EffectFor<Dep>, F: FnOnce(&mut Option<Dep>) -> Option<E>>(
191    F,
192    PhantomData<Dep>,
193);
194
195/// Conditionally register effects with a lazy initialized dependency.
196/// [`use_effect_with`] doesn't require Dependency to be [`PartialEq`],
197/// and also allows lazy initialization.
198///
199#[cfg_attr(
200    feature = "proc-macro",
201    doc = r###"
202```
203# use hooks::{hook, use_effect_with};
204#[hook(bounds = "'a")]
205fn use_effect_print<'a>(value: &'a str) {
206    use_effect_with(|old_dep| {
207        if old_dep.as_deref() == Some(value) {
208            None
209        } else {
210            *old_dep = Some(value.to_owned()); // lazily calling to_owned()
211            Some(|v: &_| println!("{}", *v))
212        }
213    })
214}
215```
216"###
217)]
218#[inline(always)]
219pub fn use_effect_with<Dep, E: EffectFor<Dep>>(
220    get_effect: impl FnOnce(&mut Option<Dep>) -> Option<E>,
221) -> UseEffectWith<Dep, E, impl FnOnce(&mut Option<Dep>) -> Option<E>> {
222    UseEffectWith(get_effect, PhantomData)
223}
224
225hooks_core::impl_hook![
226    impl<Dep, E: EffectFor<Dep>, F: FnOnce(&mut Option<Dep>) -> Option<E>> UseEffectWith<Dep, E, F> {
227        fn into_hook(self) -> Effect<Dep, E> {
228            let mut dependency = None;
229            let effect = self.0(&mut dependency);
230
231            Effect {
232                dependency,
233                inner: effect.map(EffectInner::new_registered).unwrap_or_default(),
234            }
235        }
236
237        #[inline]
238        fn update_hook(self, hook: _) {
239            hook.register_effect_if(self.0)
240        }
241        #[inline]
242        fn h(self, hook: Effect<Dep, E>) {
243            hook.register_effect_if(self.0)
244        }
245    }
246];
247
248/// An identity function which asserts argument is an [`effect fn`](EffectFor).
249#[inline]
250pub fn effect_fn<Dep, C: EffectCleanup, F: FnOnce(&Dep) -> C>(f: F) -> F {
251    f
252}
253
254#[cfg(test)]
255mod tests {
256    use std::cell::RefCell;
257
258    use futures_lite::future::block_on;
259    use hooks_core::{hook_fn, HookPollNextUpdateExt, IntoHook, UpdateHookUninitialized};
260
261    use super::{effect_fn, use_effect, use_effect_with};
262
263    #[test]
264    fn custom_hook() {
265        hook_fn!(
266            type Bounds = impl 'a;
267            fn use_test_effect<'a>(history: &'a RefCell<Vec<&'static str>>) {
268                h![use_effect(
269                    effect_fn(move |_| {
270                        // Do some effects
271                        history.borrow_mut().push("effecting");
272
273                        // Return an optional cleanup function
274                        move || history.borrow_mut().push("cleaning")
275                    }),
276                    (),
277                )]
278            }
279        );
280
281        let history = RefCell::new(Vec::with_capacity(2));
282
283        futures_lite::future::block_on(async {
284            {
285                let hook = use_test_effect(&history).into_hook_values();
286                futures_lite::pin!(hook);
287
288                assert!(hook.as_mut().next_value().await.is_some());
289
290                assert!(history.borrow().is_empty());
291
292                // poll_next_update would return false and run effect
293                // use_hook would not register because dependencies does not change
294                assert!(hook.as_mut().next_value().await.is_none());
295
296                assert_eq!(*history.borrow(), ["effecting"]);
297
298                // poll_next_update would return false and do nothing
299                // use_hook would not register because dependencies does not change
300                assert!(hook.as_mut().next_value().await.is_none());
301
302                assert_eq!(*history.borrow(), ["effecting"]);
303            }
304            // The last cleanup will be run when `hook` is dropped.
305
306            assert_eq!(history.into_inner(), ["effecting", "cleaning"]);
307        })
308    }
309
310    #[test]
311    fn test_use_effect_with() {
312        block_on(async {
313            let mut values = vec![];
314
315            {
316                let hook = super::Effect::default();
317
318                futures_lite::pin!(hook);
319
320                assert!(!hook.next_update().await);
321
322                let v = "123".to_string();
323
324                use_effect_with(|old_v| {
325                    if old_v.as_ref() == Some(&v) {
326                        None
327                    } else {
328                        *old_v = Some(v.clone());
329                        Some(|v: &String| values.push(v.clone()))
330                    }
331                })
332                .h(hook.as_mut());
333
334                assert!(!hook.next_update().await);
335
336                drop(v); // v is not moved before.
337
338                assert!(!hook.next_update().await);
339            }
340
341            assert_eq!(values, ["123"]);
342        });
343    }
344}