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}