dioxus_signals/
memo.rs

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