maple_core/reactive/
effect.rs

1use std::any::Any;
2use std::mem;
3use std::rc::Weak;
4
5use super::*;
6use std::cell::RefCell;
7use std::collections::HashSet;
8use std::hash::{Hash, Hasher};
9use std::ptr;
10use std::rc::Rc;
11
12thread_local! {
13    /// Context of the effect that is currently running. `None` if no effect is running.
14    ///
15    /// This is an array of callbacks that, when called, will add the a `Signal` to the `handle` in the argument.
16    /// The callbacks return another callback which will unsubscribe the `handle` from the `Signal`.
17    pub(super) static CONTEXTS: RefCell<Vec<Weak<RefCell<Option<Running>>>>> = RefCell::new(Vec::new());
18    pub(super) static OWNER: RefCell<Option<Owner>> = RefCell::new(None);
19}
20
21/// State of the current running effect.
22/// When the state is dropped, all dependencies are removed (both links and backlinks).
23pub(super) struct Running {
24    pub(super) execute: Rc<dyn Fn()>,
25    pub(super) dependencies: HashSet<Dependency>,
26    /// The reactive context owns all effects created within it.
27    owner: Owner,
28}
29
30impl Running {
31    /// Clears the dependencies (both links and backlinks).
32    /// Should be called when re-executing an effect to recreate all dependencies.
33    fn clear_dependencies(&mut self) {
34        for dependency in &self.dependencies {
35            dependency
36                .signal()
37                .unsubscribe(&Callback(Rc::downgrade(&self.execute)));
38        }
39        self.dependencies.clear();
40    }
41}
42
43impl Drop for Running {
44    fn drop(&mut self) {
45        self.clear_dependencies();
46    }
47}
48
49/// Owns the effects created in the current reactive scope.
50/// The effects are dropped and the cleanup callbacks are called when the [`Owner`] is dropped.
51#[derive(Default)]
52pub struct Owner {
53    effects: Vec<Rc<RefCell<Option<Running>>>>,
54    cleanup: Vec<Box<dyn FnOnce()>>,
55}
56
57impl Owner {
58    /// Create a new empty [`Owner`].
59    ///
60    /// This should be rarely used and only serve as a placeholder.
61    pub fn new() -> Self {
62        Self::default()
63    }
64
65    /// Add an effect that is owned by this [`Owner`].
66    pub(super) fn add_effect_state(&mut self, effect: Rc<RefCell<Option<Running>>>) {
67        self.effects.push(effect);
68    }
69
70    /// Add a cleanup callback that will be called when the [`Owner`] is dropped.
71    pub(super) fn add_cleanup(&mut self, cleanup: Box<dyn FnOnce()>) {
72        self.cleanup.push(cleanup);
73    }
74}
75
76impl Drop for Owner {
77    fn drop(&mut self) {
78        for effect in &self.effects {
79            effect.borrow_mut().as_mut().unwrap().clear_dependencies();
80        }
81
82        for cleanup in mem::take(&mut self.cleanup) {
83            cleanup();
84        }
85    }
86}
87
88#[derive(Clone)]
89pub(super) struct Callback(pub(super) Weak<dyn Fn()>);
90
91impl Callback {
92    #[track_caller]
93    #[must_use = "returned value must be manually called"]
94    pub fn callback(&self) -> Rc<dyn Fn()> {
95        self.try_callback().expect("callback is not valid anymore")
96    }
97
98    #[must_use = "returned value must be manually called"]
99    pub fn try_callback(&self) -> Option<Rc<dyn Fn()>> {
100        self.0.upgrade()
101    }
102}
103
104impl Hash for Callback {
105    fn hash<H: Hasher>(&self, state: &mut H) {
106        Rc::as_ptr(&self.callback()).hash(state);
107    }
108}
109
110impl PartialEq for Callback {
111    fn eq(&self, other: &Self) -> bool {
112        ptr::eq::<()>(
113            Rc::as_ptr(&self.callback()).cast(),
114            Rc::as_ptr(&other.callback()).cast(),
115        )
116    }
117}
118impl Eq for Callback {}
119
120/// A [`Weak`] backlink to a [`Signal`] for any type `T`.
121#[derive(Clone)]
122pub(super) struct Dependency(pub(super) Weak<dyn AnySignalInner>);
123
124impl Dependency {
125    fn signal(&self) -> Rc<dyn AnySignalInner> {
126        self.0.upgrade().expect("backlink should always be valid")
127    }
128}
129
130impl Hash for Dependency {
131    fn hash<H: Hasher>(&self, state: &mut H) {
132        Rc::as_ptr(&self.signal()).hash(state);
133    }
134}
135
136impl PartialEq for Dependency {
137    fn eq(&self, other: &Self) -> bool {
138        ptr::eq::<()>(
139            Rc::as_ptr(&self.signal()).cast(),
140            Rc::as_ptr(&other.signal()).cast(),
141        )
142    }
143}
144impl Eq for Dependency {}
145
146/// Creates an effect on signals used inside the effect closure.
147///
148/// Unlike [`create_effect`], this will allow the closure to run different code upon first
149/// execution, so it can return a value.
150pub fn create_effect_initial<R: 'static>(
151    initial: impl FnOnce() -> (Rc<dyn Fn()>, R) + 'static,
152) -> R {
153    type InitialFn = dyn FnOnce() -> (Rc<dyn Fn()>, Box<dyn Any>);
154
155    /// Internal implementation: use dynamic dispatch to reduce code bloat.
156    fn internal(initial: Box<InitialFn>) -> Box<dyn Any> {
157        let running: Rc<RefCell<Option<Running>>> = Rc::new(RefCell::new(None));
158
159        type MutEffect = Rc<RefCell<Option<Rc<dyn Fn()>>>>;
160        let effect: MutEffect = Rc::new(RefCell::new(None));
161        let ret: Rc<RefCell<Option<Box<dyn Any>>>> = Rc::new(RefCell::new(None));
162
163        let initial = RefCell::new(Some(initial));
164
165        let execute: Rc<dyn Fn()> = Rc::new({
166            let running = Rc::downgrade(&running);
167            let ret = Rc::downgrade(&ret);
168            move || {
169                CONTEXTS.with(|contexts| {
170                    let initial_context_size = contexts.borrow().len();
171
172                    // Upgrade running now to make sure running is valid for the whole duration of the effect.
173                    let running = running.upgrade().unwrap();
174
175                    // Recreate effect dependencies each time effect is called.
176                    running.borrow_mut().as_mut().unwrap().clear_dependencies();
177
178                    contexts.borrow_mut().push(Rc::downgrade(&running));
179
180                    if let Some(initial) = initial.take() {
181                        let effect = Rc::clone(&effect);
182                        let ret = Weak::upgrade(&ret).unwrap();
183                        let owner = create_root(move || {
184                            let (effect_tmp, ret_tmp) = initial(); // Call initial callback.
185                            *effect.borrow_mut() = Some(effect_tmp);
186                            *ret.borrow_mut() = Some(ret_tmp);
187                        });
188                        running.borrow_mut().as_mut().unwrap().owner = owner;
189                    } else {
190                        // Destroy old effects before new ones run.
191                        let old_owner = mem::replace(
192                            &mut running.borrow_mut().as_mut().unwrap().owner,
193                            Owner::new(), /* placeholder until an actual Owner is created */
194                        );
195                        drop(old_owner);
196
197                        let effect = Rc::clone(&effect);
198                        let owner = create_root(move || {
199                            effect.borrow().as_ref().unwrap()();
200                        });
201                        running.borrow_mut().as_mut().unwrap().owner = owner;
202                    }
203
204                    // Attach new dependencies.
205                    for dependency in &running.borrow().as_ref().unwrap().dependencies {
206                        dependency.signal().subscribe(Callback(Rc::downgrade(
207                            &running.borrow().as_ref().unwrap().execute,
208                        )));
209                    }
210
211                    // Remove reactive context.
212                    contexts.borrow_mut().pop();
213
214                    debug_assert_eq!(
215                        initial_context_size,
216                        contexts.borrow().len(),
217                        "context size should not change before and after create_effect_initial"
218                    );
219                });
220            }
221        });
222
223        *running.borrow_mut() = Some(Running {
224            execute: Rc::clone(&execute),
225            dependencies: HashSet::new(),
226            owner: Owner::new(),
227        });
228        debug_assert_eq!(
229            Rc::strong_count(&running),
230            1,
231            "Running should be owned exclusively by owner"
232        );
233
234        OWNER.with(|owner| {
235            if owner.borrow().is_some() {
236                owner
237                    .borrow_mut()
238                    .as_mut()
239                    .unwrap()
240                    .add_effect_state(running);
241            } else {
242                #[cfg(all(target_arch = "wasm32", debug_assertions))]
243                web_sys::console::warn_1(
244                    &"Effects created outside of a reactive root will never get disposed.".into(),
245                );
246                #[cfg(all(not(target_arch = "wasm32"), debug_assertions))]
247                eprintln!(
248                    "WARNING: Effects created outside of a reactive root will never get dropped."
249                );
250                Rc::into_raw(running); // leak running
251            }
252        });
253
254        execute();
255
256        let ret = Rc::try_unwrap(ret).expect("ret should only have 1 strong reference");
257        ret.into_inner().unwrap()
258    }
259
260    let ret = internal(Box::new(|| {
261        let (effect, ret) = initial();
262        (effect, Box::new(ret))
263    }));
264
265    *ret.downcast::<R>().unwrap()
266}
267
268/// Creates an effect on signals used inside the effect closure.
269///
270/// # Example
271/// ```
272/// use maple_core::prelude::*;
273///
274/// let state = Signal::new(0);
275///
276/// create_effect(cloned!((state) => move || {
277///     println!("State changed. New state value = {}", state.get());
278/// })); // Prints "State changed. New state value = 0"
279///
280/// state.set(1); // Prints "State changed. New state value = 1"
281/// ```
282pub fn create_effect<F>(effect: F)
283where
284    F: Fn() + 'static,
285{
286    /// Internal implementation: use dynamic dispatch to reduce code bloat.
287    fn internal(effect: Rc<dyn Fn()>) {
288        create_effect_initial(move || {
289            effect();
290            (effect, ())
291        })
292    }
293
294    internal(Rc::new(effect));
295}
296
297/// Creates a memoized value from some signals. Also know as "derived stores".
298///
299/// # Example
300/// ```
301/// use maple_core::prelude::*;
302///
303/// let state = Signal::new(0);
304///
305/// let double = create_memo(cloned!((state) => move || *state.get() * 2));
306/// assert_eq!(*double.get(), 0);
307///
308/// state.set(1);
309/// assert_eq!(*double.get(), 2);
310/// ```
311pub fn create_memo<F, Out>(derived: F) -> StateHandle<Out>
312where
313    F: Fn() -> Out + 'static,
314    Out: 'static,
315{
316    create_selector_with(derived, |_, _| false)
317}
318
319/// Creates a memoized value from some signals. Also know as "derived stores".
320/// Unlike [`create_memo`], this function will not notify dependents of a change if the output is the same.
321/// That is why the output of the function must implement [`PartialEq`].
322///
323/// To specify a custom comparison function, use [`create_selector_with`].
324pub fn create_selector<F, Out>(derived: F) -> StateHandle<Out>
325where
326    F: Fn() -> Out + 'static,
327    Out: PartialEq + 'static,
328{
329    create_selector_with(derived, PartialEq::eq)
330}
331
332/// Creates a memoized value from some signals. Also know as "derived stores".
333/// Unlike [`create_memo`], this function will not notify dependents of a change if the output is the same.
334///
335/// It takes a comparison function to compare the old and new value, which returns `true` if they
336/// are the same and `false` otherwise.
337///
338/// To use the type's [`PartialEq`] implementation instead of a custom function, use
339/// [`create_selector`].
340pub fn create_selector_with<F, Out, C>(derived: F, comparator: C) -> StateHandle<Out>
341where
342    F: Fn() -> Out + 'static,
343    Out: 'static,
344    C: Fn(&Out, &Out) -> bool + 'static,
345{
346    let derived = Rc::new(derived);
347    let comparator = Rc::new(comparator);
348
349    create_effect_initial(move || {
350        let memo = Signal::new(derived());
351
352        let effect = Rc::new({
353            let memo = memo.clone();
354            let derived = Rc::clone(&derived);
355            move || {
356                let new_value = derived();
357                if !comparator(&memo.get_untracked(), &new_value) {
358                    memo.set(new_value);
359                }
360            }
361        });
362
363        (effect, memo.into_handle())
364    })
365}
366
367/// Run the passed closure inside an untracked scope.
368///
369/// See also [`StateHandle::get_untracked()`].
370///
371/// # Example
372///
373/// ```
374/// use maple_core::prelude::*;
375///
376/// let state = Signal::new(1);
377///
378/// let double = create_memo({
379///     let state = state.clone();
380///     move || untrack(|| *state.get() * 2)
381/// });
382///
383/// assert_eq!(*double.get(), 2);
384///
385/// state.set(2);
386/// // double value should still be old value because state was untracked
387/// assert_eq!(*double.get(), 2);
388/// ```
389pub fn untrack<T>(f: impl FnOnce() -> T) -> T {
390    CONTEXTS.with(|contexts| {
391        let tmp = contexts.take();
392
393        let ret = f();
394
395        *contexts.borrow_mut() = tmp;
396
397        ret
398    })
399}
400
401/// Adds a callback function to the current reactive scope's cleanup.
402///
403/// # Example
404/// ```
405/// use maple_core::prelude::*;
406///
407/// let cleanup_called = Signal::new(false);
408///
409/// let owner = create_root(cloned!((cleanup_called) => move || {
410///     on_cleanup(move || {
411///         cleanup_called.set(true);
412///     })
413/// }));
414///
415/// assert_eq!(*cleanup_called.get(), false);
416///
417/// drop(owner);
418/// assert_eq!(*cleanup_called.get(), true);
419/// ```
420pub fn on_cleanup(f: impl FnOnce() + 'static) {
421    OWNER.with(|owner| {
422        if owner.borrow().is_some() {
423            owner
424                .borrow_mut()
425                .as_mut()
426                .unwrap()
427                .add_cleanup(Box::new(f));
428        } else {
429            #[cfg(all(target_arch = "wasm32", debug_assertions))]
430            web_sys::console::warn_1(
431                &"Cleanup callbacks created outside of a reactive root will never run.".into(),
432            );
433            #[cfg(all(not(target_arch = "wasm32"), debug_assertions))]
434            eprintln!(
435                "WARNING: Cleanup callbacks created outside of a reactive root will never run."
436            );
437        }
438    });
439}
440
441#[cfg(test)]
442mod tests {
443    use super::*;
444    use crate::cloned;
445
446    #[test]
447    fn effects() {
448        let state = Signal::new(0);
449
450        let double = Signal::new(-1);
451
452        create_effect(cloned!((state, double) => move || {
453            double.set(*state.get() * 2);
454        }));
455        assert_eq!(*double.get(), 0); // calling create_effect should call the effect at least once
456
457        state.set(1);
458        assert_eq!(*double.get(), 2);
459        state.set(2);
460        assert_eq!(*double.get(), 4);
461    }
462
463    // FIXME: cycle detection is currently broken
464    #[test]
465    #[ignore]
466    #[should_panic(expected = "cannot create cyclic dependency")]
467    fn cyclic_effects_fail() {
468        let state = Signal::new(0);
469
470        create_effect(cloned!((state) => move || {
471            state.set(*state.get() + 1);
472        }));
473
474        state.set(1);
475    }
476
477    #[test]
478    #[ignore]
479    #[should_panic(expected = "cannot create cyclic dependency")]
480    fn cyclic_effects_fail_2() {
481        let state = Signal::new(0);
482
483        create_effect(cloned!((state) => move || {
484            let value = *state.get();
485            state.set(value + 1);
486        }));
487
488        state.set(1);
489    }
490
491    #[test]
492    fn effect_should_subscribe_once() {
493        let state = Signal::new(0);
494
495        let counter = Signal::new(0);
496        create_effect(cloned!((state, counter) => move || {
497            counter.set(*counter.get_untracked() + 1);
498
499            // call state.get() twice but should subscribe once
500            state.get();
501            state.get();
502        }));
503
504        assert_eq!(*counter.get(), 1);
505
506        state.set(1);
507        assert_eq!(*counter.get(), 2);
508    }
509
510    #[test]
511    fn effect_should_recreate_dependencies() {
512        let condition = Signal::new(true);
513
514        let state1 = Signal::new(0);
515        let state2 = Signal::new(1);
516
517        let counter = Signal::new(0);
518        create_effect(cloned!((condition, state1, state2, counter) => move || {
519            counter.set(*counter.get_untracked() + 1);
520
521            if *condition.get() {
522                state1.get();
523            } else {
524                state2.get();
525            }
526        }));
527
528        assert_eq!(*counter.get(), 1);
529
530        state1.set(1);
531        assert_eq!(*counter.get(), 2);
532
533        state2.set(1);
534        assert_eq!(*counter.get(), 2); // not tracked
535
536        condition.set(false);
537        assert_eq!(*counter.get(), 3);
538
539        state1.set(2);
540        assert_eq!(*counter.get(), 3); // not tracked
541
542        state2.set(2);
543        assert_eq!(*counter.get(), 4); // tracked after condition.set
544    }
545
546    #[test]
547    fn nested_effects_should_recreate_inner() {
548        let counter = Signal::new(0);
549
550        let trigger = Signal::new(());
551
552        create_effect(cloned!((trigger, counter) => move || {
553            trigger.get(); // subscribe to trigger
554
555            create_effect(cloned!((counter) => move || {
556                counter.set(*counter.get_untracked() + 1);
557            }));
558        }));
559
560        assert_eq!(*counter.get(), 1);
561
562        trigger.set(());
563        assert_eq!(*counter.get(), 2); // old inner effect should be destroyed and thus not executed
564    }
565
566    #[test]
567    fn destroy_effects_on_owner_drop() {
568        let counter = Signal::new(0);
569
570        let trigger = Signal::new(());
571
572        let owner = create_root(cloned!((trigger, counter) => move || {
573            create_effect(move || {
574                trigger.get(); // subscribe to trigger
575                counter.set(*counter.get_untracked() + 1);
576            });
577        }));
578
579        assert_eq!(*counter.get(), 1);
580
581        trigger.set(());
582        assert_eq!(*counter.get(), 2);
583
584        drop(owner);
585        trigger.set(());
586        assert_eq!(*counter.get(), 2); // inner effect should be destroyed and thus not executed
587    }
588
589    #[test]
590    fn memo() {
591        let state = Signal::new(0);
592
593        let double = create_memo(cloned!((state) => move || *state.get() * 2));
594        assert_eq!(*double.get(), 0);
595
596        state.set(1);
597        assert_eq!(*double.get(), 2);
598
599        state.set(2);
600        assert_eq!(*double.get(), 4);
601    }
602
603    #[test]
604    /// Make sure value is memoized rather than executed on demand.
605    fn memo_only_run_once() {
606        let state = Signal::new(0);
607
608        let counter = Signal::new(0);
609        let double = create_memo(cloned!((state, counter) => move || {
610            counter.set(*counter.get_untracked() + 1);
611
612            *state.get() * 2
613        }));
614        assert_eq!(*counter.get(), 1); // once for calculating initial derived state
615
616        state.set(2);
617        assert_eq!(*counter.get(), 2);
618        assert_eq!(*double.get(), 4);
619        assert_eq!(*counter.get(), 2); // should still be 2 after access
620    }
621
622    #[test]
623    fn dependency_on_memo() {
624        let state = Signal::new(0);
625
626        let double = create_memo(cloned!((state) => move || *state.get() * 2));
627
628        let quadruple = create_memo(move || *double.get() * 2);
629
630        assert_eq!(*quadruple.get(), 0);
631
632        state.set(1);
633        assert_eq!(*quadruple.get(), 4);
634    }
635
636    #[test]
637    fn untracked_memo() {
638        let state = Signal::new(1);
639
640        let double = create_memo(cloned!((state) => move || *state.get_untracked() * 2));
641
642        assert_eq!(*double.get(), 2);
643
644        state.set(2);
645        assert_eq!(*double.get(), 2); // double value should still be true because state.get() was inside untracked
646    }
647
648    #[test]
649    fn selector() {
650        let state = Signal::new(0);
651
652        let double = create_selector(cloned!((state) => move || *state.get() * 2));
653
654        let counter = Signal::new(0);
655        create_effect(cloned!((counter, double) => move || {
656            counter.set(*counter.get_untracked() + 1);
657
658            double.get();
659        }));
660        assert_eq!(*double.get(), 0);
661        assert_eq!(*counter.get(), 1);
662
663        state.set(0);
664        assert_eq!(*double.get(), 0);
665        assert_eq!(*counter.get(), 1); // calling set_state should not trigger the effect
666
667        state.set(2);
668        assert_eq!(*double.get(), 4);
669        assert_eq!(*counter.get(), 2);
670    }
671
672    #[test]
673    fn cleanup() {
674        let cleanup_called = Signal::new(false);
675        let owner = create_root(cloned!((cleanup_called) => move || {
676            on_cleanup(move || {
677                cleanup_called.set(true);
678            })
679        }));
680
681        assert_eq!(*cleanup_called.get(), false);
682
683        drop(owner);
684        assert_eq!(*cleanup_called.get(), true);
685    }
686
687    #[test]
688    fn cleanup_in_effect() {
689        let trigger = Signal::new(());
690
691        let counter = Signal::new(0);
692
693        create_effect(cloned!((trigger, counter) => move || {
694            trigger.get(); // subscribe to trigger
695
696            on_cleanup(cloned!((counter) => move || {
697                counter.set(*counter.get() + 1);
698            }));
699        }));
700
701        assert_eq!(*counter.get(), 0);
702
703        trigger.set(());
704        assert_eq!(*counter.get(), 1);
705
706        trigger.set(());
707        assert_eq!(*counter.get(), 2);
708    }
709}