reactive_cache/
effect.rs

1use std::rc::Rc;
2
3use crate::effect_stack::{effect_peak, effect_pop, effect_push};
4
5/// A reactive effect that runs a closure whenever its dependencies change.
6///
7/// `Effect` behaves similarly to an "event listener" or a callback,
8/// but it is automatically tied to any signals or memos it reads during execution.
9/// When those dependencies change, the effect will re-run.
10///
11/// Note: The closure runs **immediately upon creation** via [`Effect::new`],
12/// so the effect is always initialized with an up-to-date value.
13///
14/// In short:
15/// - Like a callback: wraps a closure and runs it.
16/// - Adds tracking: automatically re-runs when dependent signals change.
17/// - Runs once immediately at creation.
18///
19/// # Examples
20///
21/// ## Basic usage
22/// ```
23/// use std::{cell::Cell, rc::Rc};
24/// use reactive_cache::Effect;
25///
26/// let counter = Rc::new(Cell::new(0));
27/// let c_clone = counter.clone();
28///
29/// let effect = Effect::new(move || {
30///     // This closure runs immediately
31///     c_clone.set(c_clone.get() + 1);
32/// });
33///
34/// assert_eq!(counter.get(), 1);
35/// ```
36///
37/// ## Using inside a struct
38/// ```
39/// use std::{rc::Rc, cell::Cell};
40/// use reactive_cache::{Signal, Memo, Effect};
41///
42/// struct ViewModel {
43///     counter: Rc<Signal<i32>>,
44///     double: Rc<Memo<i32>>,
45///     effect: Rc<Effect>,
46///     run_count: Rc<Cell<u32>>,
47/// }
48///
49/// let counter = Signal::new(1);
50/// let double = Memo::new({
51///     let counter = counter.clone();
52///     move || *counter.get() * 2
53/// });
54///
55/// let run_count = Rc::new(Cell::new(0));
56/// let run_count_clone = run_count.clone();
57///
58/// let effect = Effect::new({
59///     let double = double.clone();
60///     move || {
61///         run_count_clone.set(run_count_clone.get() + 1);
62///         let _ = double.get();
63///     }
64/// });
65///
66/// let vm = ViewModel {
67///     counter: counter.clone(),
68///     double: double.clone(),
69///     effect: effect,
70///     run_count: run_count.clone(),
71/// };
72///
73/// assert_eq!(run_count.get(), 1);
74/// vm.counter.set(4);
75/// assert_eq!(run_count.get(), 2);
76/// ```
77pub struct Effect {
78    f: Box<dyn Fn()>,
79}
80
81impl Effect {
82    /// Creates a new `Effect`, wrapping the provided closure
83    /// and running it immediately for dependency tracking.
84    ///
85    /// Returns an `Rc<Effect>` so the effect can be stored and shared
86    /// as a non-generic type.
87    ///
88    /// # Examples
89    ///
90    /// ## Basic usage
91    /// ```
92    /// use std::{cell::Cell, rc::Rc};
93    /// use reactive_cache::Effect;
94    ///
95    /// let counter = Rc::new(Cell::new(0));
96    /// let c_clone = counter.clone();
97    ///
98    /// let effect = Effect::new(move || {
99    ///     // This closure runs immediately
100    ///     c_clone.set(c_clone.get() + 1);
101    /// });
102    ///
103    /// assert_eq!(counter.get(), 1);
104    /// ```
105    ///
106    /// ## Using inside a struct
107    /// ```
108    /// use std::rc::Rc;
109    /// use reactive_cache::{Signal, Memo, Effect};
110    ///
111    /// struct ViewModel {
112    ///     counter: Rc<Signal<i32>>,
113    ///     double: Rc<Memo<i32>>,
114    ///     effect: Rc<Effect>,
115    /// }
116    ///
117    /// let counter = Signal::new(1);
118    /// let double = Memo::new({
119    ///     let counter = counter.clone();
120    ///     move || *counter.get() * 2
121    /// });
122    ///
123    /// let vm = ViewModel {
124    ///     counter: counter.clone(),
125    ///     double: double.clone(),
126    ///     effect: Effect::new({
127    ///         let double = double.clone();
128    ///         move || println!("Double is {}", double.get())
129    ///     }),
130    /// };
131    ///
132    /// counter.set(3);
133    /// assert_eq!(double.get(), 6);
134    /// ```
135    #[allow(clippy::new_ret_no_self)]
136    pub fn new(f: impl Fn() + 'static) -> Rc<Effect> {
137        let e: Rc<Effect> = Rc::new(Effect { f: Box::new(f) });
138        let w = Rc::downgrade(&e);
139
140        // Dependency collection only at creation time
141        effect_push(w.clone(), true);
142        e.run();
143        effect_pop(w.clone(), true);
144
145        e
146    }
147
148    /// Creates a new `Effect` with an additional dependency initializer.
149    ///
150    /// This works like [`Effect::new`], but requires a `deps` closure to be provided,
151    /// which will be executed during the initial dependency collection phase.
152    ///
153    /// **Important:** Dependency tracking is performed **only when running `deps`**,
154    /// not `f`. The closure `f` will still be executed when dependencies change,
155    /// but its execution does **not** collect new dependencies.
156    ///
157    /// This is useful when your effect closure contains conditional logic
158    /// (e.g. `if`/`match`), and you want to ensure that *all possible branches*
159    /// have their dependencies tracked on the first run.
160    ///
161    /// Returns an `Rc<Effect>` so the effect can be stored and shared
162    /// as a non-generic type.
163    ///
164    /// # Examples
165    ///
166    /// ```
167    /// use std::{cell::Cell, rc::Rc};
168    /// use reactive_cache::Effect;
169    /// use reactive_macros::{ref_signal, signal};
170    ///
171    /// signal!(static mut FLAG: bool = true;);
172    /// signal!(static mut COUNTER: i32 = 10;);
173    ///
174    /// let result = Rc::new(Cell::new(0));
175    /// let r_clone = result.clone();
176    ///
177    /// // Effect closure has a conditional branch
178    /// let effect = Effect::new_with_deps(
179    ///     move || {
180    ///         match *FLAG_get() {
181    ///             true => {}
182    ///             false => {
183    ///                 r_clone.set(*COUNTER_get());
184    ///             }
185    ///         }
186    ///     },
187    ///     // Explicitly declare both `FLAG` and `COUNTER` as dependencies
188    ///     move || {
189    ///         FLAG();
190    ///         COUNTER();
191    ///     },
192    /// );
193    ///
194    /// assert_eq!(result.get(), 0); // runs with FLAG = true
195    ///
196    /// // Changing `FLAG` to false will trigger the effect
197    /// FLAG_set(false);
198    /// assert_eq!(result.get(), 10);
199    ///
200    /// // Changing `COUNTER` still triggers the effect, even though
201    /// // `FLAG` was true on the first run.
202    /// COUNTER_set(20);
203    /// assert_eq!(result.get(), 20);
204    /// ```
205    pub fn new_with_deps(f: impl Fn() + 'static, deps: impl Fn()) -> Rc<Effect> {
206        let e: Rc<Effect> = Rc::new(Effect { f: Box::new(f) });
207        let w = Rc::downgrade(&e);
208
209        // Dependency collection only at creation time
210        effect_push(w.clone(), true);
211        deps();
212        effect_pop(w.clone(), true);
213
214        // If there is an additional dependency initializer,
215        // the `Effect` needs to be run immediately
216        // after dependency collection is completed.
217        run_untracked(&e);
218
219        e
220    }
221
222    /// Runs the effect closure.
223    ///
224    /// Typically called by the reactive system when dependencies change.
225    ///
226    /// # Notes
227    ///
228    /// After initialization, any call to an `Effect` must go through `run()`.
229    /// Since the preconditions for executing `run()` differ depending on context
230    /// (e.g. dependency collection vs. signal-triggered updates), such calls
231    /// must be handled with care.
232    ///
233    /// Dependency collection for an `Effect` should be limited to its directly
234    /// connected signals. The intended call chain is:
235    ///
236    /// `Effect → Memo(s) → Signal(s)`
237    ///
238    /// In this model, the `Effect` must always be the root of the chain.
239    /// Other `Effect`s should not be tracked as dependencies, and runs triggered
240    /// by signals should not themselves cause further dependency collection.
241    fn run(&self) {
242        assert!(
243            std::ptr::eq(&*effect_peak().unwrap().effect.upgrade().unwrap(), self),
244            "`Effect` is not pushed onto the stack before being called."
245        );
246
247        (self.f)()
248    }
249}
250
251pub(crate) fn run_untracked(e: &Rc<Effect>) {
252    let w = Rc::downgrade(e);
253
254    effect_push(w.clone(), false);
255    e.run();
256    effect_pop(w.clone(), false);
257}