reactive_graph/computed/
memo.rs

1use super::ArcMemo;
2use crate::{
3    owner::{ArenaItem, FromLocal, LocalStorage, Storage, SyncStorage},
4    signal::{
5        guards::{Mapped, Plain, ReadGuard},
6        ArcReadSignal,
7    },
8    traits::{DefinedAt, Dispose, Get, ReadUntracked, Track},
9    unwrap_signal,
10};
11use std::{fmt::Debug, hash::Hash, panic::Location};
12
13/// A memo is an efficient derived reactive value based on other reactive values.
14///
15/// Unlike a "derived signal," a memo comes with two guarantees:
16/// 1. The memo will only run *once* per change, no matter how many times you
17///    access its value.
18/// 2. The memo will only notify its dependents if the value of the computation changes.
19///
20/// This makes a memo the perfect tool for expensive computations.
21///
22/// Memos have a certain overhead compared to derived signals. In most cases, you should
23/// create a derived signal. But if the derivation calculation is expensive, you should
24/// create a memo.
25///
26/// Memos are lazy: they do not run at all until they are read for the first time, and they will
27/// not re-run the calculation when a source signal changes until they are read again.
28///
29/// This is an arena-allocated type, which is `Copy` and is disposed when its reactive
30/// [`Owner`](crate::owner::Owner) cleans up. For a reference-counted signal that lives as
31/// as long as a reference to it is alive, see [`ArcMemo`].
32///
33/// ```
34/// # use reactive_graph::prelude::*;
35/// # use reactive_graph::computed::Memo;
36/// # use reactive_graph::effect::Effect;
37/// # use reactive_graph::signal::signal;
38/// # tokio_test::block_on(async move {
39/// # any_spawner::Executor::init_tokio(); let owner = reactive_graph::owner::Owner::new(); owner.set();
40/// # tokio::task::LocalSet::new().run_until(async {
41/// # fn really_expensive_computation(value: i32) -> i32 { value };
42/// let (value, set_value) = signal(0);
43///
44/// // πŸ†— we could create a derived signal with a simple function
45/// let double_value = move || value.get() * 2;
46/// set_value.set(2);
47/// assert_eq!(double_value(), 4);
48///
49/// // but imagine the computation is really expensive
50/// let expensive = move || really_expensive_computation(value.get()); // lazy: doesn't run until called
51/// Effect::new(move |_| {
52///   // πŸ†— run #1: calls `really_expensive_computation` the first time
53///   println!("expensive = {}", expensive());
54/// });
55/// Effect::new(move |_| {
56///   // ❌ run #2: this calls `really_expensive_computation` a second time!
57///   let value = expensive();
58///   // do something else...
59/// });
60///
61/// // instead, we create a memo
62/// // πŸ†— run #1: the calculation runs once immediately
63/// let memoized = Memo::new(move |_| really_expensive_computation(value.get()));
64/// Effect::new(move |_| {
65///   // πŸ†— reads the current value of the memo
66///   //    can be `memoized()` on nightly
67///   println!("memoized = {}", memoized.get());
68/// });
69/// Effect::new(move |_| {
70///   // βœ… reads the current value **without re-running the calculation**
71///   let value = memoized.get();
72///   // do something else...
73/// });
74/// # });
75/// # });
76/// ```
77///
78/// ## Core Trait Implementations
79/// - [`.get()`](crate::traits::Get) clones the current value of the memo.
80///   If you call it within an effect, it will cause that effect to subscribe
81///   to the memo, and to re-run whenever the value of the memo changes.
82///   - [`.get_untracked()`](crate::traits::GetUntracked) clones the value of
83///     the memo without reactively tracking it.
84/// - [`.read()`](crate::traits::Read) returns a guard that allows accessing the
85///   value of the memo by reference. If you call it within an effect, it will
86///   cause that effect to subscribe to the memo, and to re-run whenever the
87///   value of the memo changes.
88///   - [`.read_untracked()`](crate::traits::ReadUntracked) gives access to the
89///     current value of the memo without reactively tracking it.
90/// - [`.with()`](crate::traits::With) allows you to reactively access the memo’s
91///   value without cloning by applying a callback function.
92///   - [`.with_untracked()`](crate::traits::WithUntracked) allows you to access
93///     the memo’s value by applying a callback function without reactively
94///     tracking it.
95/// - [`.to_stream()`](crate::traits::ToStream) converts the memo to an `async`
96///   stream of values.
97/// - [`::from_stream()`](crate::traits::FromStream) converts an `async` stream
98///   of values into a memo containing the latest value.
99pub struct Memo<T, S = SyncStorage>
100where
101    S: Storage<T>,
102{
103    #[cfg(any(debug_assertions, leptos_debuginfo))]
104    defined_at: &'static Location<'static>,
105    inner: ArenaItem<ArcMemo<T, S>, S>,
106}
107
108impl<T, S> Dispose for Memo<T, S>
109where
110    S: Storage<T>,
111{
112    fn dispose(self) {
113        self.inner.dispose()
114    }
115}
116
117impl<T> From<ArcMemo<T, SyncStorage>> for Memo<T>
118where
119    T: Send + Sync + 'static,
120{
121    #[track_caller]
122    fn from(value: ArcMemo<T, SyncStorage>) -> Self {
123        Self {
124            #[cfg(any(debug_assertions, leptos_debuginfo))]
125            defined_at: Location::caller(),
126            inner: ArenaItem::new_with_storage(value),
127        }
128    }
129}
130
131impl<T> FromLocal<ArcMemo<T, LocalStorage>> for Memo<T, LocalStorage>
132where
133    T: 'static,
134{
135    #[track_caller]
136    fn from_local(value: ArcMemo<T, LocalStorage>) -> Self {
137        Self {
138            #[cfg(any(debug_assertions, leptos_debuginfo))]
139            defined_at: Location::caller(),
140            inner: ArenaItem::new_with_storage(value),
141        }
142    }
143}
144
145impl<T> Memo<T>
146where
147    T: Send + Sync + 'static,
148{
149    #[track_caller]
150    #[cfg_attr(
151        feature = "tracing",
152        tracing::instrument(level = "debug", skip_all)
153    )]
154    /// Creates a new memoized, computed reactive value.
155    ///
156    /// As with an [`Effect`](crate::effect::Effect), the argument to the memo function is the previous value,
157    /// i.e., the current value of the memo, which will be `None` for the initial calculation.
158    /// ```
159    /// # use reactive_graph::prelude::*;
160    /// # use reactive_graph::computed::Memo;
161    /// # use reactive_graph::effect::Effect;
162    /// # use reactive_graph::signal::signal;
163    /// # tokio_test::block_on(async move {
164    /// # any_spawner::Executor::init_tokio(); let owner = reactive_graph::owner::Owner::new(); owner.set();
165    /// # fn really_expensive_computation(value: i32) -> i32 { value };
166    /// let (value, set_value) = signal(0);
167    ///
168    /// // the memo will reactively update whenever `value` changes
169    /// let memoized =
170    ///     Memo::new(move |_| really_expensive_computation(value.get()));
171    /// # });
172    /// ```
173    pub fn new(fun: impl Fn(Option<&T>) -> T + Send + Sync + 'static) -> Self
174    where
175        T: PartialEq,
176    {
177        Self {
178            #[cfg(any(debug_assertions, leptos_debuginfo))]
179            defined_at: Location::caller(),
180            inner: ArenaItem::new_with_storage(ArcMemo::new(fun)),
181        }
182    }
183
184    #[track_caller]
185    #[cfg_attr(
186        feature = "tracing",
187        tracing::instrument(level = "trace", skip_all)
188    )]
189    /// Creates a new memo with a custom comparison function. By default, memos simply use
190    /// [`PartialEq`] to compare the previous value to the new value. Passing a custom comparator
191    /// allows you to compare the old and new values using any criteria.
192    ///
193    /// `changed` should be a function that returns `true` if the new value is different from the
194    /// old value.
195    pub fn new_with_compare(
196        fun: impl Fn(Option<&T>) -> T + Send + Sync + 'static,
197        changed: fn(Option<&T>, Option<&T>) -> bool,
198    ) -> Self {
199        Self {
200            #[cfg(any(debug_assertions, leptos_debuginfo))]
201            defined_at: Location::caller(),
202            inner: ArenaItem::new_with_storage(ArcMemo::new_with_compare(
203                fun, changed,
204            )),
205        }
206    }
207
208    /// Creates a new memo by passing a function that computes the value.
209    ///
210    /// Unlike [`Memo::new`](), this receives ownership of the previous value. As a result, it
211    /// must return both the new value and a `bool` that is `true` if the value has changed.
212    ///
213    /// This is lazy: the function will not be called until the memo's value is read for the first
214    /// time.
215    #[track_caller]
216    #[cfg_attr(
217        feature = "tracing",
218        tracing::instrument(level = "trace", skip_all)
219    )]
220    pub fn new_owning(
221        fun: impl Fn(Option<T>) -> (T, bool) + Send + Sync + 'static,
222    ) -> Self {
223        Self {
224            #[cfg(any(debug_assertions, leptos_debuginfo))]
225            defined_at: Location::caller(),
226            inner: ArenaItem::new_with_storage(ArcMemo::new_owning(fun)),
227        }
228    }
229}
230
231impl<T, S> Copy for Memo<T, S> where S: Storage<T> {}
232
233impl<T, S> Clone for Memo<T, S>
234where
235    S: Storage<T>,
236{
237    fn clone(&self) -> Self {
238        *self
239    }
240}
241
242impl<T, S> Debug for Memo<T, S>
243where
244    S: Debug + Storage<T>,
245{
246    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
247        f.debug_struct("Memo")
248            .field("type", &std::any::type_name::<T>())
249            .field("store", &self.inner)
250            .finish()
251    }
252}
253
254impl<T, S> PartialEq for Memo<T, S>
255where
256    S: Storage<T>,
257{
258    fn eq(&self, other: &Self) -> bool {
259        self.inner == other.inner
260    }
261}
262
263impl<T, S> Eq for Memo<T, S> where S: Storage<T> {}
264
265impl<T, S> Hash for Memo<T, S>
266where
267    S: Storage<T>,
268{
269    fn hash<H: std::hash::Hasher>(&self, state: &mut H) {
270        self.inner.hash(state);
271    }
272}
273
274impl<T, S> DefinedAt for Memo<T, S>
275where
276    S: Storage<T>,
277{
278    fn defined_at(&self) -> Option<&'static Location<'static>> {
279        #[cfg(any(debug_assertions, leptos_debuginfo))]
280        {
281            Some(self.defined_at)
282        }
283        #[cfg(not(any(debug_assertions, leptos_debuginfo)))]
284        {
285            None
286        }
287    }
288}
289
290impl<T, S> Track for Memo<T, S>
291where
292    T: 'static,
293    S: Storage<ArcMemo<T, S>> + Storage<T>,
294    ArcMemo<T, S>: Track,
295{
296    #[track_caller]
297    fn track(&self) {
298        if let Some(inner) = self.inner.try_get_value() {
299            inner.track();
300        }
301    }
302}
303
304impl<T, S> ReadUntracked for Memo<T, S>
305where
306    T: 'static,
307    S: Storage<ArcMemo<T, S>> + Storage<T>,
308{
309    type Value =
310        ReadGuard<T, Mapped<Plain<Option<<S as Storage<T>>::Wrapped>>, T>>;
311
312    fn try_read_untracked(&self) -> Option<Self::Value> {
313        self.inner
314            .try_get_value()
315            .map(|inner| inner.read_untracked())
316    }
317}
318
319impl<T, S> From<Memo<T, S>> for ArcMemo<T, S>
320where
321    T: 'static,
322    S: Storage<ArcMemo<T, S>> + Storage<T>,
323{
324    #[track_caller]
325    fn from(value: Memo<T, S>) -> Self {
326        value
327            .inner
328            .try_get_value()
329            .unwrap_or_else(unwrap_signal!(value))
330    }
331}
332
333impl<T> From<ArcReadSignal<T>> for Memo<T>
334where
335    T: Clone + PartialEq + Send + Sync + 'static,
336{
337    #[track_caller]
338    fn from(value: ArcReadSignal<T>) -> Self {
339        Memo::new(move |_| value.get())
340    }
341}