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}