sycamore_reactive/
effects.rs

1//! Side effects!
2
3use std::cell::RefCell;
4use std::rc::Rc;
5
6use crate::create_memo;
7
8/// Creates an effect on signals used inside the effect closure.
9///
10/// # Example
11/// ```
12/// # use sycamore_reactive::*;
13/// # create_root(|| {
14/// let state = create_signal(0);
15///
16/// create_effect(move || {
17///     println!("new state = {}", state.get());
18/// });
19/// // Prints "new state = 0"
20///
21/// state.set(1);
22/// // Prints "new state = 1"
23/// # });
24/// ```
25///
26/// `create_effect` should only be used for creating **side-effects**. It is generally not
27/// recommended to update signal states inside an effect. You probably should be using a
28/// [`create_memo`](crate::create_memo) instead.
29#[cfg_attr(debug_assertions, track_caller)]
30pub fn create_effect(f: impl FnMut() + 'static) {
31    create_memo(f);
32}
33
34/// Creates an effect that runs a different code path on the first run.
35///
36/// The initial function is expected to return a tuple containing a function for subsequent runs
37/// and an optional value that will be returned by the effect.
38///
39/// # Example
40/// ```
41/// # use sycamore_reactive::*;
42/// # create_root(|| {
43/// let state = create_signal(0);
44///
45/// let initial_value = create_effect_initial(move || {
46///     state.set(100);
47///     (
48///         Box::new(move || state.set(state.get() + 1)),
49///         state.get(), // This value will be returned and assigned to `initial_value`.
50///     )
51/// });
52/// # });
53/// ```
54///
55/// Note that the initial function is also called within the effect scope. This means that signals
56/// created within the initial function will no longer be alive in subsequet runs. If you want to
57/// create signals that are alive in subsequent runs, you should use
58/// [`use_current_scope`](crate::use_current_scope) and
59/// [`NodeHandle::run_in`](crate::NodeHandle::run_in).
60#[cfg_attr(debug_assertions, track_caller)]
61pub fn create_effect_initial<T: 'static>(
62    initial: impl FnOnce() -> (Box<dyn FnMut() + 'static>, T) + 'static,
63) -> T {
64    let ret = Rc::new(RefCell::new(None));
65    let mut initial = Some(initial);
66    let mut effect = None;
67
68    create_effect({
69        let ret = Rc::clone(&ret);
70        move || {
71            if let Some(initial) = initial.take() {
72                let (new_f, value) = initial();
73                effect = Some(new_f);
74                *ret.borrow_mut() = Some(value);
75            } else {
76                effect.as_mut().unwrap()()
77            }
78        }
79    });
80
81    ret.take().unwrap()
82}
83
84#[cfg(test)]
85mod tests {
86    use crate::*;
87
88    #[test]
89    fn effect() {
90        let _ = create_root(|| {
91            let state = create_signal(0);
92
93            let double = create_signal(-1);
94
95            create_effect(move || {
96                double.set(state.get() * 2);
97            });
98            assert_eq!(double.get(), 0); // calling create_effect should call the effect at least once
99
100            state.set(1);
101            assert_eq!(double.get(), 2);
102            state.set(2);
103            assert_eq!(double.get(), 4);
104        });
105    }
106
107    #[test]
108    fn effect_with_explicit_dependencies() {
109        let _ = create_root(|| {
110            let state = create_signal(0);
111
112            let double = create_signal(-1);
113
114            create_effect(on(state, move || {
115                double.set(state.get() * 2);
116            }));
117            assert_eq!(double.get(), 0); // calling create_effect should call the effect at least once
118
119            state.set(1);
120            assert_eq!(double.get(), 2);
121            state.set(2);
122            assert_eq!(double.get(), 4);
123        });
124    }
125
126    #[test]
127    fn effect_cannot_create_infinite_loop() {
128        let _ = create_root(|| {
129            let state = create_signal(0);
130            create_effect(move || {
131                state.track();
132                state.set(0);
133            });
134            state.set(0);
135        });
136    }
137
138    #[test]
139    fn effect_should_only_subscribe_once_to_same_signal() {
140        let _ = create_root(|| {
141            let state = create_signal(0);
142
143            let counter = create_signal(0);
144            create_effect(move || {
145                counter.set(counter.get_untracked() + 1);
146
147                // call state.track() twice but should subscribe once
148                state.track();
149                state.track();
150            });
151
152            assert_eq!(counter.get(), 1);
153
154            state.set(1);
155            assert_eq!(counter.get(), 2);
156        });
157    }
158
159    #[test]
160    fn effect_should_recreate_dependencies_each_time() {
161        let _ = create_root(|| {
162            let condition = create_signal(true);
163
164            let state1 = create_signal(0);
165            let state2 = create_signal(1);
166
167            let counter = create_signal(0);
168            create_effect(move || {
169                counter.set(counter.get_untracked() + 1);
170
171                if condition.get() {
172                    state1.track();
173                } else {
174                    state2.track();
175                }
176            });
177
178            assert_eq!(counter.get(), 1);
179
180            state1.set(1);
181            assert_eq!(counter.get(), 2);
182
183            state2.set(1);
184            assert_eq!(counter.get(), 2); // not tracked
185
186            condition.set(false);
187            assert_eq!(counter.get(), 3);
188
189            state1.set(2);
190            assert_eq!(counter.get(), 3); // not tracked
191
192            state2.set(2);
193            assert_eq!(counter.get(), 4); // tracked after condition.set
194        });
195    }
196
197    #[test]
198    fn outer_effects_run_first() {
199        let _ = create_root(|| {
200            let trigger = create_signal(());
201
202            let outer_counter = create_signal(0);
203            let inner_counter = create_signal(0);
204
205            create_effect(move || {
206                trigger.track();
207                outer_counter.set(outer_counter.get_untracked() + 1);
208
209                create_effect(move || {
210                    trigger.track();
211                    inner_counter.set(inner_counter.get_untracked() + 1);
212                });
213            });
214
215            assert_eq!(outer_counter.get(), 1);
216            assert_eq!(inner_counter.get(), 1);
217
218            trigger.set(());
219
220            assert_eq!(outer_counter.get(), 2);
221            assert_eq!(inner_counter.get(), 2);
222        });
223    }
224
225    #[test]
226    fn destroy_effects_on_scope_dispose() {
227        let _ = create_root(|| {
228            let counter = create_signal(0);
229
230            let trigger = create_signal(());
231
232            let child_scope = create_child_scope(move || {
233                create_effect(move || {
234                    trigger.track();
235                    counter.set(counter.get_untracked() + 1);
236                });
237            });
238
239            assert_eq!(counter.get(), 1);
240
241            trigger.set(());
242            assert_eq!(counter.get(), 2);
243
244            child_scope.dispose();
245            trigger.set(());
246            assert_eq!(counter.get(), 2); // inner effect should be destroyed and thus not executed
247        });
248    }
249
250    #[test]
251    fn effect_scoped_subscribing_to_own_signal() {
252        let _ = create_root(|| {
253            let trigger = create_signal(());
254            create_effect(move || {
255                trigger.track();
256                let signal = create_signal(());
257                // Track own signal:
258                signal.track();
259            });
260            trigger.set(());
261        });
262    }
263}