dioxus_signals/
memo.rs

1use crate::CopyValue;
2use crate::{read::Readable, ReadableRef, Signal};
3use crate::{read_impls, GlobalMemo, ReadableExt, WritableExt};
4use std::{
5    cell::RefCell,
6    ops::Deref,
7    sync::{atomic::AtomicBool, Arc},
8};
9
10use dioxus_core::{
11    current_scope_id, spawn_isomorphic, IntoAttributeValue, IntoDynNode, ReactiveContext, ScopeId,
12    Subscribers,
13};
14use futures_util::StreamExt;
15use generational_box::{AnyStorage, BorrowResult, UnsyncStorage};
16
17struct UpdateInformation<T> {
18    dirty: Arc<AtomicBool>,
19    callback: RefCell<Box<dyn FnMut() -> T>>,
20}
21
22#[doc = include_str!("../docs/memo.md")]
23#[doc(alias = "Selector")]
24#[doc(alias = "UseMemo")]
25#[doc(alias = "Memorize")]
26pub struct Memo<T> {
27    inner: Signal<T>,
28    update: CopyValue<UpdateInformation<T>>,
29}
30
31impl<T> Memo<T> {
32    /// Create a new memo
33    #[track_caller]
34    pub fn new(f: impl FnMut() -> T + 'static) -> Self
35    where
36        T: PartialEq + 'static,
37    {
38        Self::new_with_location(f, std::panic::Location::caller())
39    }
40
41    /// Create a new memo with an explicit location
42    pub fn new_with_location(
43        mut f: impl FnMut() -> T + 'static,
44        location: &'static std::panic::Location<'static>,
45    ) -> Self
46    where
47        T: PartialEq + 'static,
48    {
49        let dirty = Arc::new(AtomicBool::new(false));
50        let (tx, mut rx) = futures_channel::mpsc::unbounded();
51
52        let callback = {
53            let dirty = dirty.clone();
54            move || {
55                dirty.store(true, std::sync::atomic::Ordering::Relaxed);
56                let _ = tx.unbounded_send(());
57            }
58        };
59        let rc = ReactiveContext::new_with_callback(callback, current_scope_id(), location);
60
61        // Create a new signal in that context, wiring up its dependencies and subscribers
62        let mut recompute = move || rc.reset_and_run_in(&mut f);
63        let value = recompute();
64        let recompute = RefCell::new(Box::new(recompute) as Box<dyn FnMut() -> T>);
65        let update = CopyValue::new(UpdateInformation {
66            dirty,
67            callback: recompute,
68        });
69        let state: Signal<T> = Signal::new_with_caller(value, location);
70
71        let memo = Memo {
72            inner: state,
73            update,
74        };
75
76        spawn_isomorphic(async move {
77            while rx.next().await.is_some() {
78                // Remove any pending updates
79                while rx.try_next().is_ok() {}
80                memo.recompute();
81            }
82        });
83
84        memo
85    }
86
87    /// Creates a new [`GlobalMemo`] that can be used anywhere inside your dioxus app. This memo will automatically be created once per app the first time you use it.
88    ///
89    /// # Example
90    /// ```rust, no_run
91    /// # use dioxus::prelude::*;
92    /// static SIGNAL: GlobalSignal<i32> = Signal::global(|| 0);
93    /// // Create a new global memo that can be used anywhere in your app
94    /// static DOUBLED: GlobalMemo<i32> = Memo::global(|| SIGNAL() * 2);
95    ///
96    /// fn App() -> Element {
97    ///     rsx! {
98    ///         button {
99    ///             // When SIGNAL changes, the memo will update because the SIGNAL is read inside DOUBLED
100    ///             onclick: move |_| *SIGNAL.write() += 1,
101    ///             "{DOUBLED}"
102    ///         }
103    ///     }
104    /// }
105    /// ```
106    ///
107    /// <div class="warning">
108    ///
109    /// Global memos are generally not recommended for use in libraries because it makes it more difficult to allow multiple instances of components you define in your library.
110    ///
111    /// </div>
112    #[track_caller]
113    pub const fn global(constructor: fn() -> T) -> GlobalMemo<T>
114    where
115        T: PartialEq + 'static,
116    {
117        GlobalMemo::new(constructor)
118    }
119
120    /// Rerun the computation and update the value of the memo if the result has changed.
121    #[tracing::instrument(skip(self))]
122    fn recompute(&self)
123    where
124        T: PartialEq + 'static,
125    {
126        let mut update_copy = self.update;
127        let update_write = update_copy.write();
128        let peak = self.inner.peek();
129        let new_value = (update_write.callback.borrow_mut())();
130        if new_value != *peak {
131            drop(peak);
132            let mut copy = self.inner;
133            copy.set(new_value);
134        }
135        // Always mark the memo as no longer dirty even if the value didn't change
136        update_write
137            .dirty
138            .store(false, std::sync::atomic::Ordering::Relaxed);
139    }
140
141    /// Get the scope that the signal was created in.
142    pub fn origin_scope(&self) -> ScopeId
143    where
144        T: 'static,
145    {
146        self.inner.origin_scope()
147    }
148
149    /// Get the id of the signal.
150    pub fn id(&self) -> generational_box::GenerationalBoxId
151    where
152        T: 'static,
153    {
154        self.inner.id()
155    }
156}
157
158impl<T> Readable for Memo<T>
159where
160    T: PartialEq,
161{
162    type Target = T;
163    type Storage = UnsyncStorage;
164
165    #[track_caller]
166    fn try_read_unchecked(
167        &self,
168    ) -> Result<ReadableRef<'static, Self>, generational_box::BorrowError>
169    where
170        T: 'static,
171    {
172        // Read the inner generational box instead of the signal so we have more fine grained control over exactly when the subscription happens
173        let read = self.inner.inner.try_read_unchecked()?;
174
175        let needs_update = self
176            .update
177            .read()
178            .dirty
179            .swap(false, std::sync::atomic::Ordering::Relaxed);
180        let result = if needs_update {
181            drop(read);
182            // We shouldn't be subscribed to the value here so we don't trigger the scope we are currently in to rerun even though that scope got the latest value because we synchronously update the value: https://github.com/DioxusLabs/dioxus/issues/2416
183            self.recompute();
184            self.inner.inner.try_read_unchecked()
185        } else {
186            Ok(read)
187        };
188        // Subscribe to the current scope before returning the value
189        if let Ok(read) = &result {
190            if let Some(reactive_context) = ReactiveContext::current() {
191                tracing::trace!("Subscribing to the reactive context {}", reactive_context);
192                reactive_context.subscribe(read.subscribers.clone());
193            }
194        }
195        result.map(|read| <UnsyncStorage as AnyStorage>::map(read, |v| &v.value))
196    }
197
198    /// Get the current value of the signal. **Unlike read, this will not subscribe the current scope to the signal which can cause parts of your UI to not update.**
199    ///
200    /// If the signal has been dropped, this will panic.
201    #[track_caller]
202    fn try_peek_unchecked(&self) -> BorrowResult<ReadableRef<'static, Self>>
203    where
204        T: 'static,
205    {
206        self.inner.try_peek_unchecked()
207    }
208
209    fn subscribers(&self) -> Subscribers
210    where
211        T: 'static,
212    {
213        self.inner.subscribers()
214    }
215}
216
217impl<T> IntoAttributeValue for Memo<T>
218where
219    T: Clone + IntoAttributeValue + PartialEq + 'static,
220{
221    fn into_value(self) -> dioxus_core::AttributeValue {
222        self.with(|f| f.clone().into_value())
223    }
224}
225
226impl<T> IntoDynNode for Memo<T>
227where
228    T: Clone + IntoDynNode + PartialEq + 'static,
229{
230    fn into_dyn_node(self) -> dioxus_core::DynamicNode {
231        self().into_dyn_node()
232    }
233}
234
235impl<T: 'static> PartialEq for Memo<T> {
236    fn eq(&self, other: &Self) -> bool {
237        self.inner == other.inner
238    }
239}
240
241impl<T: Clone> Deref for Memo<T>
242where
243    T: PartialEq + 'static,
244{
245    type Target = dyn Fn() -> T;
246
247    fn deref(&self) -> &Self::Target {
248        unsafe { ReadableExt::deref_impl(self) }
249    }
250}
251
252read_impls!(Memo<T> where T: PartialEq);
253
254impl<T> Clone for Memo<T> {
255    fn clone(&self) -> Self {
256        *self
257    }
258}
259
260impl<T> Copy for Memo<T> {}