sycamore_reactive/
memos.rs

1//! Memos (aka. eager derived signals).
2
3use std::cell::RefCell;
4
5use crate::{create_empty_signal, create_signal, ReadSignal, Root};
6
7/// Creates a memoized value from some signals.
8/// Unlike [`create_memo`], this function will not notify dependents of a
9/// change if the output is the same.
10///
11/// It takes a comparison function to compare the old and new value, which returns `true` if
12/// they are the same and `false` otherwise.
13///
14/// To use the type's [`PartialEq`] implementation instead of a custom function, use
15/// [`create_selector`].
16#[cfg_attr(debug_assertions, track_caller)]
17pub fn create_selector_with<T>(
18    mut f: impl FnMut() -> T + 'static,
19    mut eq: impl FnMut(&T, &T) -> bool + 'static,
20) -> ReadSignal<T> {
21    let root = Root::global();
22    let signal = create_empty_signal();
23    let prev = root.current_node.replace(signal.id);
24    let (initial, tracker) = root.tracked_scope(&mut f);
25    root.current_node.set(prev);
26
27    tracker.create_dependency_link(root, signal.id);
28
29    let mut signal_mut = signal.get_mut();
30    signal_mut.value = Some(Box::new(initial));
31    signal_mut.callback = Some(Box::new(move |value| {
32        let value = value.downcast_mut().expect("wrong memo type");
33        let new = f();
34        if eq(&new, value) {
35            false
36        } else {
37            *value = new;
38            true
39        }
40    }));
41
42    *signal
43}
44
45/// Creates a memoized computation from some signals.
46///
47/// The output is derived from all the signals that are used within the memo closure.
48/// If any of the tracked signals are updated, the memo is also updated.
49///
50/// # Difference from derived signals
51///
52/// Derived signals (functions referencing signals) are lazy and do not keep track of the result
53/// of the computation. This means that the computation will not be executed until needed.
54/// This also means that calling the derived signal twice will result in the same computation
55/// twice.
56///
57/// ```
58/// # use sycamore_reactive::*;
59/// # create_root(|| {
60/// let state = create_signal(0);
61/// let double = || state.get() * 2;
62///
63/// let _ = double();
64/// // Here, the closure named double is called again.
65/// // If the computation is expensive enough, this would be wasted work!
66/// let _ = double();
67/// # });
68/// ```
69///
70/// Memos, on the other hand, are eagerly evaluated and will only run the computation when one
71/// of its dependencies change.
72///
73/// Memos also incur a slightly higher performance penalty than simple derived signals, so unless
74/// there is some computation involved, it will likely be faster to just use a derived signal.
75///
76/// # Example
77/// ```
78/// # use sycamore_reactive::*;
79/// # create_root(|| {
80/// let state = create_signal(0);
81/// let double = create_memo(move || state.get() * 2);
82///
83/// assert_eq!(double.get(), 0);
84/// state.set(1);
85/// assert_eq!(double.get(), 2);
86/// # });
87/// ```
88#[cfg_attr(debug_assertions, track_caller)]
89pub fn create_memo<T>(f: impl FnMut() -> T + 'static) -> ReadSignal<T> {
90    create_selector_with(f, |_, _| false)
91}
92
93/// Creates a memoized value from some signals.
94///
95/// Unlike [`create_memo`], this function will not notify dependents of a hange if the output is the
96/// same. That is why the output of the function must implement [`PartialEq`].
97///
98/// To specify a custom comparison function, use [`create_selector_with`].
99///
100/// # Example
101/// ```
102/// # use sycamore_reactive::*;
103/// # create_root(|| {
104/// let state = create_signal(1);
105/// let squared = create_selector(move || state.get() * state.get());
106/// assert_eq!(squared.get(), 1);
107///
108/// create_effect(move || println!("x^2 = {}", squared.get()));
109///
110/// state.set(2); // Triggers the effect.
111/// assert_eq!(squared.get(), 4);
112///
113/// state.set(-2); // Does not trigger the effect.
114/// assert_eq!(squared.get(), 4);
115/// # });
116/// ```
117#[cfg_attr(debug_assertions, track_caller)]
118pub fn create_selector<T>(f: impl FnMut() -> T + 'static) -> ReadSignal<T>
119where
120    T: PartialEq,
121{
122    create_selector_with(f, PartialEq::eq)
123}
124
125/// An alternative to [`create_signal`] that uses a reducer to get the next
126/// value.
127///
128/// It uses a reducer function that takes the previous value and a message and returns the next
129/// value.
130///
131/// Returns a [`ReadSignal`] and a dispatch function to send messages to the reducer.
132///
133/// # Params
134/// * `initial` - The initial value of the state.
135/// * `reducer` - A function that takes the previous value and a message and returns the next value.
136///
137/// # Example
138/// ```
139/// # use sycamore_reactive::*;
140/// enum Msg {
141///     Increment,
142///     Decrement,
143/// }
144///
145/// # create_root(|| {
146/// let (state, dispatch) = create_reducer(0, |&state, msg: Msg| match msg {
147///     Msg::Increment => state + 1,
148///     Msg::Decrement => state - 1,
149/// });
150///
151/// assert_eq!(state.get(), 0);
152/// dispatch(Msg::Increment);
153/// assert_eq!(state.get(), 1);
154/// dispatch(Msg::Decrement);
155/// assert_eq!(state.get(), 0);
156/// # });
157/// ```
158#[cfg_attr(debug_assertions, track_caller)]
159pub fn create_reducer<T, Msg>(
160    initial: T,
161    reduce: impl FnMut(&T, Msg) -> T,
162) -> (ReadSignal<T>, impl Fn(Msg)) {
163    let reduce = RefCell::new(reduce);
164    let signal = create_signal(initial);
165    let dispatch = move |msg| signal.update(|value| *value = reduce.borrow_mut()(value, msg));
166    (*signal, dispatch)
167}
168
169#[cfg(test)]
170mod tests {
171    use crate::*;
172
173    #[test]
174    fn memo() {
175        let _ = create_root(|| {
176            let state = create_signal(0);
177            let double = create_memo(move || state.get() * 2);
178
179            assert_eq!(double.get(), 0);
180            state.set(1);
181            assert_eq!(double.get(), 2);
182            state.set(2);
183            assert_eq!(double.get(), 4);
184        });
185    }
186
187    /// Make sure value is memoized rather than executed on demand.
188    #[test]
189    fn memo_only_run_once() {
190        let _ = create_root(|| {
191            let state = create_signal(0);
192
193            let counter = create_signal(0);
194            let double = create_memo(move || {
195                counter.set_silent(counter.get_untracked() + 1);
196                state.get() * 2
197            });
198
199            assert_eq!(counter.get(), 1); // once for calculating initial derived state
200            state.set(2);
201            assert_eq!(counter.get(), 2);
202            assert_eq!(double.get(), 4);
203            assert_eq!(counter.get(), 2); // should still be 2 after access
204        });
205    }
206
207    #[test]
208    fn dependency_on_memo() {
209        let _ = create_root(|| {
210            let state = create_signal(0);
211            let double = create_memo(move || state.get() * 2);
212            let quadruple = create_memo(move || double.get() * 2);
213
214            assert_eq!(quadruple.get(), 0);
215            state.set(1);
216            assert_eq!(quadruple.get(), 4);
217        });
218    }
219
220    #[test]
221    fn untracked_memo() {
222        let _ = create_root(|| {
223            let state = create_signal(1);
224            let double = create_memo(move || state.get_untracked() * 2);
225
226            assert_eq!(double.get(), 2);
227            state.set(2);
228            assert_eq!(double.get(), 2); // double value should still be true because state.get()
229                                         // was
230                                         // inside untracked
231        });
232    }
233
234    #[test]
235    fn memos_should_recreate_dependencies_each_time() {
236        let _ = create_root(|| {
237            let condition = create_signal(true);
238
239            let state1 = create_signal(0);
240            let state2 = create_signal(1);
241
242            let counter = create_signal(0);
243            create_memo(move || {
244                counter.set_silent(counter.get_untracked() + 1);
245
246                if condition.get() {
247                    state1.track();
248                } else {
249                    state2.track();
250                }
251            });
252
253            assert_eq!(counter.get(), 1);
254
255            state1.set(1);
256            assert_eq!(counter.get(), 2);
257
258            state2.set(1);
259            assert_eq!(counter.get(), 2); // not tracked
260
261            condition.set(false);
262            assert_eq!(counter.get(), 3);
263
264            state1.set(2);
265            assert_eq!(counter.get(), 3); // not tracked
266
267            state2.set(2);
268            assert_eq!(counter.get(), 4); // tracked after condition.set
269        });
270    }
271
272    #[test]
273    fn destroy_memos_on_scope_dispose() {
274        let _ = create_root(|| {
275            let counter = create_signal(0);
276
277            let trigger = create_signal(());
278
279            let child_scope = create_child_scope(move || {
280                let _ = create_memo(move || {
281                    trigger.track();
282                    counter.set_silent(counter.get_untracked() + 1);
283                });
284            });
285
286            assert_eq!(counter.get(), 1);
287
288            trigger.set(());
289            assert_eq!(counter.get(), 2);
290
291            child_scope.dispose();
292            trigger.set(());
293            assert_eq!(counter.get(), 2); // memo should be destroyed and thus not executed
294        });
295    }
296
297    #[test]
298    fn selector() {
299        let _ = create_root(|| {
300            let state = create_signal(0);
301            let double = create_selector(move || state.get() * 2);
302
303            let counter = create_signal(0);
304            create_effect(move || {
305                counter.set(counter.get_untracked() + 1);
306
307                double.track();
308            });
309            assert_eq!(double.get(), 0);
310            assert_eq!(counter.get(), 1);
311
312            state.set(0);
313            state.set(0);
314            state.set(0);
315            assert_eq!(double.get(), 0);
316            assert_eq!(counter.get(), 1);
317
318            state.set(2);
319            assert_eq!(double.get(), 4);
320            assert_eq!(counter.get(), 2);
321        });
322    }
323
324    #[test]
325    fn reducer() {
326        let _ = create_root(|| {
327            enum Msg {
328                Increment,
329                Decrement,
330            }
331
332            let (state, dispatch) = create_reducer(0, |state, msg: Msg| match msg {
333                Msg::Increment => *state + 1,
334                Msg::Decrement => *state - 1,
335            });
336
337            assert_eq!(state.get(), 0);
338            dispatch(Msg::Increment);
339            assert_eq!(state.get(), 1);
340            dispatch(Msg::Decrement);
341            assert_eq!(state.get(), 0);
342            dispatch(Msg::Increment);
343            dispatch(Msg::Increment);
344            assert_eq!(state.get(), 2);
345        });
346    }
347
348    #[test]
349    fn memo_reducer() {
350        let _ = create_root(|| {
351            enum Msg {
352                Increment,
353                Decrement,
354            }
355
356            let (state, dispatch) = create_reducer(0, |state, msg: Msg| match msg {
357                Msg::Increment => *state + 1,
358                Msg::Decrement => *state - 1,
359            });
360            let doubled = create_memo(move || state.get() * 2);
361
362            assert_eq!(doubled.get(), 0);
363            dispatch(Msg::Increment);
364            assert_eq!(doubled.get(), 2);
365            dispatch(Msg::Decrement);
366            assert_eq!(doubled.get(), 0);
367        });
368    }
369}