leptos_reactive/memo.rs
1use crate::{
2 create_isomorphic_effect, diagnostics::AccessDiagnostics, node::NodeId,
3 on_cleanup, with_runtime, AnyComputation, Runtime, SignalDispose,
4 SignalGet, SignalGetUntracked, SignalStream, SignalWith,
5 SignalWithUntracked,
6};
7use std::{any::Any, cell::RefCell, fmt, marker::PhantomData, rc::Rc};
8
9// IMPLEMENTATION NOTE:
10// Memos are implemented "lazily," i.e., the inner computation is not run
11// when the memo is created or when its value is marked as stale, but on demand
12// when it is accessed, if the value is stale. This means that the value is stored
13// internally as Option<T>, even though it can always be accessed by the user as T.
14// This means the inner value can be unwrapped in circumstances in which we know
15// `Runtime::update_if_necessary()` has already been called, e.g., in the
16// `.try_with_no_subscription()` calls below that are unwrapped with
17// `.expect("invariant: must have already been initialized")`.
18
19/// Creates an efficient derived reactive value based on other reactive values.
20///
21/// Unlike a "derived signal," a memo comes with two guarantees:
22/// 1. The memo will only run *once* per change, no matter how many times you
23/// access its value.
24/// 2. The memo will only notify its dependents if the value of the computation changes.
25///
26/// This makes a memo the perfect tool for expensive computations.
27///
28/// Memos have a certain overhead compared to derived signals. In most cases, you should
29/// create a derived signal. But if the derivation calculation is expensive, you should
30/// create a memo.
31///
32/// As with [`create_effect`](crate::create_effect), the argument to the memo function is the previous value,
33/// i.e., the current value of the memo, which will be `None` for the initial calculation.
34///
35/// ```
36/// # use leptos_reactive::*;
37/// # fn really_expensive_computation(value: i32) -> i32 { value };
38/// # let runtime = create_runtime();
39/// let (value, set_value) = create_signal(0);
40///
41/// // π we could create a derived signal with a simple function
42/// let double_value = move || value.get() * 2;
43/// set_value.set(2);
44/// assert_eq!(double_value(), 4);
45///
46/// // but imagine the computation is really expensive
47/// let expensive = move || really_expensive_computation(value.get()); // lazy: doesn't run until called
48/// create_effect(move |_| {
49/// // π run #1: calls `really_expensive_computation` the first time
50/// log::debug!("expensive = {}", expensive());
51/// });
52/// create_effect(move |_| {
53/// // β run #2: this calls `really_expensive_computation` a second time!
54/// let value = expensive();
55/// // do something else...
56/// });
57///
58/// // instead, we create a memo
59/// // π run #1: the calculation runs once immediately
60/// let memoized = create_memo(move |_| really_expensive_computation(value.get()));
61/// create_effect(move |_| {
62/// // π reads the current value of the memo
63/// // can be `memoized()` on nightly
64/// log::debug!("memoized = {}", memoized.get());
65/// });
66/// create_effect(move |_| {
67/// // β
reads the current value **without re-running the calculation**
68/// let value = memoized.get();
69/// // do something else...
70/// });
71/// # runtime.dispose();
72/// ```
73#[cfg_attr(
74 any(debug_assertions, feature="ssr"),
75 instrument(
76 level = "trace",
77 skip_all,
78 fields(
79 ty = %std::any::type_name::<T>()
80 )
81 )
82)]
83#[track_caller]
84#[inline(always)]
85pub fn create_memo<T>(f: impl Fn(Option<&T>) -> T + 'static) -> Memo<T>
86where
87 T: PartialEq + 'static,
88{
89 Runtime::current().create_owning_memo(move |current_value| {
90 let new_value = f(current_value.as_ref());
91 let is_different = current_value.as_ref() != Some(&new_value);
92 (new_value, is_different)
93 })
94}
95
96/// Like [`create_memo`], `create_owning_memo` creates an efficient derived reactive value based on
97/// other reactive values, but with two differences:
98/// 1. The argument to the memo function is owned instead of borrowed.
99/// 2. The function must also return whether the value has changed, as the first element of the tuple.
100///
101/// All of the other caveats and guarantees are the same as the usual "borrowing" memos.
102///
103/// This type of memo is useful for memos which can avoid computation by re-using the last value,
104/// especially slices that need to allocate.
105///
106/// ```
107/// # use leptos_reactive::*;
108/// # fn really_expensive_computation(value: i32) -> i32 { value };
109/// # let runtime = create_runtime();
110/// pub struct State {
111/// name: String,
112/// token: String,
113/// }
114///
115/// let state = create_rw_signal(State {
116/// name: "Alice".to_owned(),
117/// token: "abcdef".to_owned(),
118/// });
119///
120/// // If we used `create_memo`, we'd need to allocate every time the state changes, but by using
121/// // `create_owning_memo` we can allocate only when `state.name` changes.
122/// let name = create_owning_memo(move |old_name| {
123/// state.with(move |state| {
124/// if let Some(name) =
125/// old_name.filter(|old_name| old_name == &state.name)
126/// {
127/// (name, false)
128/// } else {
129/// (state.name.clone(), true)
130/// }
131/// })
132/// });
133/// let set_name = move |name| state.update(|state| state.name = name);
134///
135/// // We can also re-use the last allocation even when the value changes, which is usually faster,
136/// // but may have some caveats (e.g. if the value size is drastically reduced, the memory will
137/// // still be used for the life of the memo).
138/// let token = create_owning_memo(move |old_token| {
139/// state.with(move |state| {
140/// let is_different = old_token.as_ref() != Some(&state.token);
141/// let mut token = old_token.unwrap_or_else(String::new);
142///
143/// if is_different {
144/// token.clone_from(&state.token);
145/// }
146/// (token, is_different)
147/// })
148/// });
149/// let set_token = move |new_token| state.update(|state| state.token = new_token);
150/// # runtime.dispose();
151/// ```
152#[cfg_attr(
153 any(debug_assertions, feature="ssr"),
154 instrument(
155 level = "trace",
156 skip_all,
157 fields(
158 ty = %std::any::type_name::<T>()
159 )
160 )
161)]
162#[track_caller]
163#[inline(always)]
164pub fn create_owning_memo<T>(
165 f: impl Fn(Option<T>) -> (T, bool) + 'static,
166) -> Memo<T>
167where
168 T: 'static,
169{
170 Runtime::current().create_owning_memo(f)
171}
172
173/// An efficient derived reactive value based on other reactive values.
174///
175/// Unlike a "derived signal," a memo comes with two guarantees:
176/// 1. The memo will only run *once* per change, no matter how many times you
177/// access its value.
178/// 2. The memo will only notify its dependents if the value of the computation changes.
179///
180/// This makes a memo the perfect tool for expensive computations.
181///
182/// Memos have a certain overhead compared to derived signals. In most cases, you should
183/// create a derived signal. But if the derivation calculation is expensive, you should
184/// create a memo.
185///
186/// As with [`create_effect`](crate::create_effect), the argument to the memo function is the previous value,
187/// i.e., the current value of the memo, which will be `None` for the initial calculation.
188///
189/// ## Core Trait Implementations
190/// - [`.get()`](#impl-SignalGet<T>-for-Memo<T>) (or calling the signal as a function) clones the current
191/// value of the signal. If you call it within an effect, it will cause that effect
192/// to subscribe to the signal, and to re-run whenever the value of the signal changes.
193/// - [`.get_untracked()`](#impl-SignalGetUntracked<T>-for-Memo<T>) clones the value of the signal
194/// without reactively tracking it.
195/// - [`.with()`](#impl-SignalWith<T>-for-Memo<T>) allows you to reactively access the signalβs value without
196/// cloning by applying a callback function.
197/// - [`.with_untracked()`](#impl-SignalWithUntracked<T>-for-Memo<T>) allows you to access the signalβs
198/// value without reactively tracking it.
199/// - [`.to_stream()`](#impl-SignalStream<T>-for-Memo<T>) converts the signal to an `async` stream of values.
200///
201/// ## Examples
202/// ```
203/// # use leptos_reactive::*;
204/// # fn really_expensive_computation(value: i32) -> i32 { value };
205/// # let runtime = create_runtime();
206/// let (value, set_value) = create_signal(0);
207///
208/// // π we could create a derived signal with a simple function
209/// let double_value = move || value.get() * 2;
210/// set_value.set(2);
211/// assert_eq!(double_value(), 4);
212///
213/// // but imagine the computation is really expensive
214/// let expensive = move || really_expensive_computation(value.get()); // lazy: doesn't run until called
215/// create_effect(move |_| {
216/// // π run #1: calls `really_expensive_computation` the first time
217/// log::debug!("expensive = {}", expensive());
218/// });
219/// create_effect(move |_| {
220/// // β run #2: this calls `really_expensive_computation` a second time!
221/// let value = expensive();
222/// // do something else...
223/// });
224///
225/// // instead, we create a memo
226/// // π run #1: the calculation runs once immediately
227/// let memoized = create_memo(move |_| really_expensive_computation(value.get()));
228/// create_effect(move |_| {
229/// // π reads the current value of the memo
230/// log::debug!("memoized = {}", memoized.get());
231/// });
232/// create_effect(move |_| {
233/// // β
reads the current value **without re-running the calculation**
234/// // can be `memoized()` on nightly
235/// let value = memoized.get();
236/// // do something else...
237/// });
238/// # runtime.dispose();
239/// ```
240pub struct Memo<T>
241where
242 T: 'static,
243{
244 pub(crate) id: NodeId,
245 pub(crate) ty: PhantomData<T>,
246 #[cfg(any(debug_assertions, feature = "ssr"))]
247 pub(crate) defined_at: &'static std::panic::Location<'static>,
248}
249
250impl<T> Memo<T> {
251 /// Creates a new memo from the given function.
252 ///
253 /// This is identical to [`create_memo`].
254 /// ```
255 /// # use leptos_reactive::*;
256 /// # fn really_expensive_computation(value: i32) -> i32 { value };
257 /// # let runtime = create_runtime();
258 /// let value = RwSignal::new(0);
259 ///
260 /// // π we could create a derived signal with a simple function
261 /// let double_value = move || value.get() * 2;
262 /// value.set(2);
263 /// assert_eq!(double_value(), 4);
264 ///
265 /// // but imagine the computation is really expensive
266 /// let expensive = move || really_expensive_computation(value.get()); // lazy: doesn't run until called
267 /// Effect::new(move |_| {
268 /// // π run #1: calls `really_expensive_computation` the first time
269 /// log::debug!("expensive = {}", expensive());
270 /// });
271 /// Effect::new(move |_| {
272 /// // β run #2: this calls `really_expensive_computation` a second time!
273 /// let value = expensive();
274 /// // do something else...
275 /// });
276 ///
277 /// // instead, we create a memo
278 /// // π run #1: the calculation runs once immediately
279 /// let memoized = Memo::new(move |_| really_expensive_computation(value.get()));
280 /// Effect::new(move |_| {
281 /// // π reads the current value of the memo
282 /// // can be `memoized()` on nightly
283 /// log::debug!("memoized = {}", memoized.get());
284 /// });
285 /// Effect::new(move |_| {
286 /// // β
reads the current value **without re-running the calculation**
287 /// let value = memoized.get();
288 /// // do something else...
289 /// });
290 /// # runtime.dispose();
291 /// ```
292 #[inline(always)]
293 #[track_caller]
294 pub fn new(f: impl Fn(Option<&T>) -> T + 'static) -> Memo<T>
295 where
296 T: PartialEq + 'static,
297 {
298 create_memo(f)
299 }
300
301 /// Creates a new owning memo from the given function.
302 ///
303 /// This is identical to [`create_owning_memo`].
304 ///
305 /// ```
306 /// # use leptos_reactive::*;
307 /// # fn really_expensive_computation(value: i32) -> i32 { value };
308 /// # let runtime = create_runtime();
309 /// pub struct State {
310 /// name: String,
311 /// token: String,
312 /// }
313 ///
314 /// let state = RwSignal::new(State {
315 /// name: "Alice".to_owned(),
316 /// token: "abcdef".to_owned(),
317 /// });
318 ///
319 /// // If we used `Memo::new`, we'd need to allocate every time the state changes, but by using
320 /// // `Memo::new_owning` we can allocate only when `state.name` changes.
321 /// let name = Memo::new_owning(move |old_name| {
322 /// state.with(move |state| {
323 /// if let Some(name) =
324 /// old_name.filter(|old_name| old_name == &state.name)
325 /// {
326 /// (name, false)
327 /// } else {
328 /// (state.name.clone(), true)
329 /// }
330 /// })
331 /// });
332 /// let set_name = move |name| state.update(|state| state.name = name);
333 ///
334 /// // We can also re-use the last allocation even when the value changes, which is usually faster,
335 /// // but may have some caveats (e.g. if the value size is drastically reduced, the memory will
336 /// // still be used for the life of the memo).
337 /// let token = Memo::new_owning(move |old_token| {
338 /// state.with(move |state| {
339 /// let is_different = old_token.as_ref() != Some(&state.token);
340 /// let mut token = old_token.unwrap_or_else(String::new);
341 ///
342 /// if is_different {
343 /// token.clone_from(&state.token);
344 /// }
345 /// (token, is_different)
346 /// })
347 /// });
348 /// let set_token = move |new_token| state.update(|state| state.token = new_token);
349 /// # runtime.dispose();
350 /// ```
351 #[inline(always)]
352 #[track_caller]
353 pub fn new_owning(f: impl Fn(Option<T>) -> (T, bool) + 'static) -> Memo<T>
354 where
355 T: 'static,
356 {
357 create_owning_memo(f)
358 }
359}
360
361impl<T> Clone for Memo<T>
362where
363 T: 'static,
364{
365 fn clone(&self) -> Self {
366 *self
367 }
368}
369
370impl<T> Copy for Memo<T> {}
371
372impl<T> fmt::Debug for Memo<T> {
373 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
374 let mut s = f.debug_struct("Memo");
375 s.field("id", &self.id);
376 s.field("ty", &self.ty);
377 #[cfg(any(debug_assertions, feature = "ssr"))]
378 s.field("defined_at", &self.defined_at);
379 s.finish()
380 }
381}
382
383impl<T> Eq for Memo<T> {}
384
385impl<T> PartialEq for Memo<T> {
386 fn eq(&self, other: &Self) -> bool {
387 self.id == other.id
388 }
389}
390
391fn forward_ref_to<T, O, F: FnOnce(&T) -> O>(
392 f: F,
393) -> impl FnOnce(&Option<T>) -> O {
394 |maybe_value: &Option<T>| {
395 let ref_t = maybe_value
396 .as_ref()
397 .expect("invariant: must have already been initialized");
398 f(ref_t)
399 }
400}
401
402impl<T: Clone> SignalGetUntracked for Memo<T> {
403 type Value = T;
404
405 #[cfg_attr(
406 any(debug_assertions, feature = "ssr"),
407 instrument(
408 level = "trace",
409 name = "Memo::get_untracked()",
410 skip_all,
411 fields(
412 id = ?self.id,
413 defined_at = %self.defined_at,
414 ty = %std::any::type_name::<T>()
415 )
416 )
417 )]
418 fn get_untracked(&self) -> T {
419 with_runtime(move |runtime| {
420 let f = |maybe_value: &Option<T>| {
421 maybe_value
422 .clone()
423 .expect("invariant: must have already been initialized")
424 };
425 match self.id.try_with_no_subscription(runtime, f) {
426 Ok(t) => t,
427 Err(_) => panic_getting_dead_memo(
428 #[cfg(any(debug_assertions, feature = "ssr"))]
429 self.defined_at,
430 ),
431 }
432 })
433 .expect("runtime to be alive")
434 }
435
436 #[cfg_attr(
437 any(debug_assertions, feature = "ssr"),
438 instrument(
439 level = "trace",
440 name = "Memo::try_get_untracked()",
441 skip_all,
442 fields(
443 id = ?self.id,
444 defined_at = %self.defined_at,
445 ty = %std::any::type_name::<T>()
446 )
447 )
448 )]
449 #[inline(always)]
450 fn try_get_untracked(&self) -> Option<T> {
451 self.try_with_untracked(T::clone)
452 }
453}
454
455impl<T> SignalWithUntracked for Memo<T> {
456 type Value = T;
457
458 #[cfg_attr(
459 any(debug_assertions, feature = "ssr"),
460 instrument(
461 level = "trace",
462 name = "Memo::with_untracked()",
463 skip_all,
464 fields(
465 id = ?self.id,
466 defined_at = %self.defined_at,
467 ty = %std::any::type_name::<T>()
468 )
469 )
470 )]
471 fn with_untracked<O>(&self, f: impl FnOnce(&T) -> O) -> O {
472 with_runtime(|runtime| {
473 match self.id.try_with_no_subscription(runtime, forward_ref_to(f)) {
474 Ok(t) => t,
475 Err(_) => panic_getting_dead_memo(
476 #[cfg(any(debug_assertions, feature = "ssr"))]
477 self.defined_at,
478 ),
479 }
480 })
481 .expect("runtime to be alive")
482 }
483
484 #[cfg_attr(
485 any(debug_assertions, feature = "ssr"),
486 instrument(
487 level = "trace",
488 name = "Memo::try_with_untracked()",
489 skip_all,
490 fields(
491 id = ?self.id,
492 defined_at = %self.defined_at,
493 ty = %std::any::type_name::<T>()
494 )
495 )
496 )]
497 #[inline]
498 fn try_with_untracked<O>(&self, f: impl FnOnce(&T) -> O) -> Option<O> {
499 with_runtime(|runtime| {
500 self.id
501 .try_with_no_subscription(runtime, |v: &Option<T>| {
502 v.as_ref().map(f)
503 })
504 .ok()
505 .flatten()
506 })
507 .ok()
508 .flatten()
509 }
510}
511
512/// # Examples
513///
514/// ```
515/// # use leptos_reactive::*;
516/// # let runtime = create_runtime();
517/// let (count, set_count) = create_signal(0);
518/// let double_count = create_memo(move |_| count.get() * 2);
519///
520/// assert_eq!(double_count.get(), 0);
521/// set_count.set(1);
522///
523/// // can be `double_count()` on nightly
524/// // assert_eq!(double_count(), 2);
525/// assert_eq!(double_count.get(), 2);
526/// # runtime.dispose();
527/// #
528/// ```
529impl<T: Clone> SignalGet for Memo<T> {
530 type Value = T;
531
532 #[cfg_attr(
533 any(debug_assertions, feature = "ssr"),
534 instrument(
535 name = "Memo::get()",
536 level = "trace",
537 skip_all,
538 fields(
539 id = ?self.id,
540 defined_at = %self.defined_at,
541 ty = %std::any::type_name::<T>()
542 )
543 )
544 )]
545 #[track_caller]
546 #[inline(always)]
547 fn get(&self) -> T {
548 self.with(T::clone)
549 }
550
551 #[cfg_attr(
552 any(debug_assertions, feature = "ssr"),
553 instrument(
554 level = "trace",
555 name = "Memo::try_get()",
556 skip_all,
557 fields(
558 id = ?self.id,
559 defined_at = %self.defined_at,
560 ty = %std::any::type_name::<T>()
561 )
562 )
563 )]
564 #[track_caller]
565 #[inline(always)]
566 fn try_get(&self) -> Option<T> {
567 self.try_with(T::clone)
568 }
569}
570
571impl<T> SignalWith for Memo<T> {
572 type Value = T;
573
574 #[cfg_attr(
575 any(debug_assertions, feature = "ssr"),
576 instrument(
577 level = "trace",
578 name = "Memo::with()",
579 skip_all,
580 fields(
581 id = ?self.id,
582 defined_at = %self.defined_at,
583 ty = %std::any::type_name::<T>()
584 )
585 )
586 )]
587 #[track_caller]
588 fn with<O>(&self, f: impl FnOnce(&T) -> O) -> O {
589 match self.try_with(f) {
590 Some(t) => t,
591 None => panic_getting_dead_memo(
592 #[cfg(any(debug_assertions, feature = "ssr"))]
593 self.defined_at,
594 ),
595 }
596 }
597
598 #[cfg_attr(
599 any(debug_assertions, feature = "ssr"),
600 instrument(
601 level = "trace",
602 name = "Memo::try_with()",
603 skip_all,
604 fields(
605 id = ?self.id,
606 defined_at = %self.defined_at,
607 ty = %std::any::type_name::<T>()
608 )
609 )
610 )]
611 #[track_caller]
612 fn try_with<O>(&self, f: impl FnOnce(&T) -> O) -> Option<O> {
613 let diagnostics = diagnostics!(self);
614
615 with_runtime(|runtime| {
616 self.id.subscribe(runtime, diagnostics);
617 self.id
618 .try_with_no_subscription(runtime, forward_ref_to(f))
619 .ok()
620 })
621 .ok()
622 .flatten()
623 }
624}
625
626impl<T: Clone> SignalStream<T> for Memo<T> {
627 #[cfg_attr(
628 any(debug_assertions, feature = "ssr"),
629 instrument(
630 level = "trace",
631 name = "Memo::to_stream()",
632 skip_all,
633 fields(
634 id = ?self.id,
635 defined_at = %self.defined_at,
636 ty = %std::any::type_name::<T>()
637 )
638 )
639 )]
640 fn to_stream(&self) -> std::pin::Pin<Box<dyn futures::Stream<Item = T>>> {
641 let (tx, rx) = futures::channel::mpsc::unbounded();
642
643 let close_channel = tx.clone();
644
645 on_cleanup(move || close_channel.close_channel());
646
647 let this = *self;
648
649 create_isomorphic_effect(move |_| {
650 let _ = tx.unbounded_send(this.get());
651 });
652
653 Box::pin(rx)
654 }
655}
656
657impl<T> SignalDispose for Memo<T> {
658 fn dispose(self) {
659 _ = with_runtime(|runtime| runtime.dispose_node(self.id));
660 }
661}
662
663impl_get_fn_traits![Memo];
664
665pub(crate) struct MemoState<T, F>
666where
667 T: 'static,
668 F: Fn(Option<T>) -> (T, bool),
669{
670 pub f: F,
671 pub t: PhantomData<T>,
672 #[cfg(any(debug_assertions, feature = "ssr"))]
673 pub(crate) defined_at: &'static std::panic::Location<'static>,
674}
675
676impl<T, F> AnyComputation for MemoState<T, F>
677where
678 T: 'static,
679 F: Fn(Option<T>) -> (T, bool),
680{
681 #[cfg_attr(
682 any(debug_assertions, feature = "ssr"),
683 instrument(
684 name = "Memo::run()",
685 level = "trace",
686 skip_all,
687 fields(
688 defined_at = %self.defined_at,
689 ty = %std::any::type_name::<T>()
690 )
691 )
692 )]
693 fn run(&self, value: Rc<RefCell<dyn Any>>) -> bool {
694 let mut value = value.borrow_mut();
695 let curr_value = value
696 .downcast_mut::<Option<T>>()
697 .expect("to downcast memo value");
698
699 // run the memo
700 let (new_value, is_different) = (self.f)(curr_value.take());
701
702 // set new value
703 *curr_value = Some(new_value);
704
705 is_different
706 }
707}
708
709#[cold]
710#[inline(never)]
711#[track_caller]
712fn format_memo_warning(
713 msg: &str,
714 #[cfg(any(debug_assertions, feature = "ssr"))]
715 defined_at: &'static std::panic::Location<'static>,
716) -> String {
717 let location = std::panic::Location::caller();
718
719 let defined_at_msg = {
720 #[cfg(any(debug_assertions, feature = "ssr"))]
721 {
722 format!("signal created here: {defined_at}\n")
723 }
724
725 #[cfg(not(any(debug_assertions, feature = "ssr")))]
726 {
727 String::default()
728 }
729 };
730
731 format!("{msg}\n{defined_at_msg}warning happened here: {location}",)
732}
733
734#[cold]
735#[inline(never)]
736#[track_caller]
737pub(crate) fn panic_getting_dead_memo(
738 #[cfg(any(debug_assertions, feature = "ssr"))]
739 defined_at: &'static std::panic::Location<'static>,
740) -> ! {
741 panic!(
742 "{}",
743 format_memo_warning(
744 "Attempted to get a memo after it was disposed.",
745 #[cfg(any(debug_assertions, feature = "ssr"))]
746 defined_at,
747 )
748 )
749}