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}