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}