From 37e489623c7c5c828d09a0f3b947cad2f45b1287 Mon Sep 17 00:00:00 2001 From: Aphek Date: Thu, 28 Dec 2023 21:51:22 -0300 Subject: [PATCH] feat: Implement "raw" memo to allow memos that re-use the previous value --- leptos_reactive/src/memo.rs | 79 +++++++++++++++++++++++++--------- leptos_reactive/src/runtime.rs | 6 +-- leptos_reactive/tests/memo.rs | 58 +++++++++++++++++++++++++ 3 files changed, 120 insertions(+), 23 deletions(-) diff --git a/leptos_reactive/src/memo.rs b/leptos_reactive/src/memo.rs index a86ebce4d4..b29be6cbdc 100644 --- a/leptos_reactive/src/memo.rs +++ b/leptos_reactive/src/memo.rs @@ -86,7 +86,33 @@ pub fn create_memo(f: impl Fn(Option<&T>) -> T + 'static) -> Memo where T: PartialEq + 'static, { - Runtime::current().create_memo(f) + Runtime::current().create_raw_memo(move |current_value| { + let new_value = f(current_value.as_ref()); + let is_different = current_value.as_ref() != Some(&new_value); + (new_value, is_different) + }) +} + +#[allow(missing_docs)] // TODO +#[cfg_attr( + any(debug_assertions, feature="ssr"), + instrument( + level = "trace", + skip_all, + fields( + ty = %std::any::type_name::() + ) + ) +)] +#[track_caller] +#[inline(always)] +pub fn create_raw_memo( + f: impl Fn(Option) -> (T, bool) + 'static, +) -> Memo +where + T: PartialEq + 'static, +{ + Runtime::current().create_raw_memo(f) } /// An efficient derived reactive value based on other reactive values. @@ -216,6 +242,16 @@ impl Memo { { create_memo(f) } + + #[allow(missing_docs)] // TODO + #[inline(always)] + #[track_caller] + pub fn new_raw(f: impl Fn(Option) -> (T, bool) + 'static) -> Memo + where + T: PartialEq + 'static, + { + create_raw_memo(f) + } } impl Clone for Memo @@ -519,8 +555,8 @@ impl_get_fn_traits![Memo]; pub(crate) struct MemoState where - T: PartialEq + 'static, - F: Fn(Option<&T>) -> T, + T: 'static, + F: Fn(Option) -> (T, bool), { pub f: F, pub t: PhantomData, @@ -530,8 +566,8 @@ where impl AnyComputation for MemoState where - T: PartialEq + 'static, - F: Fn(Option<&T>) -> T, + T: 'static, + F: Fn(Option) -> (T, bool), { #[cfg_attr( any(debug_assertions, feature = "ssr"), @@ -546,24 +582,27 @@ where ) )] fn run(&self, value: Rc>) -> bool { - let (new_value, is_different) = { - let value = value.borrow(); - let curr_value = value - .downcast_ref::>() - .expect("to downcast memo value"); - - // run the effect - let new_value = (self.f)(curr_value.as_ref()); - let is_different = curr_value.as_ref() != Some(&new_value); - (new_value, is_different) - }; - if is_different { + // we defensively take and release the BorrowMut twice here + // in case a change during the memo running schedules a rerun + // ideally this should never happen, but this guards against panic + let curr_value = { + // downcast value let mut value = value.borrow_mut(); - let curr_value = value + let value = value .downcast_mut::>() .expect("to downcast memo value"); - *curr_value = Some(new_value); - } + value.take() + }; + + // run the memo + let (new_value, is_different) = (self.f)(curr_value); + + // set new value + let mut value = value.borrow_mut(); + let value = value + .downcast_mut::>() + .expect("to downcast memo value"); + *value = Some(new_value); is_different } diff --git a/leptos_reactive/src/runtime.rs b/leptos_reactive/src/runtime.rs index 9099a2bff6..07c7fea909 100644 --- a/leptos_reactive/src/runtime.rs +++ b/leptos_reactive/src/runtime.rs @@ -1191,12 +1191,12 @@ impl RuntimeId { #[track_caller] #[inline(always)] - pub(crate) fn create_memo( + pub(crate) fn create_raw_memo( self, - f: impl Fn(Option<&T>) -> T + 'static, + f: impl Fn(Option) -> (T, bool) + 'static, ) -> Memo where - T: PartialEq + Any + 'static, + T: 'static, { Memo { id: self.create_concrete_memo( diff --git a/leptos_reactive/tests/memo.rs b/leptos_reactive/tests/memo.rs index 35eb8d9a89..dbe131859d 100644 --- a/leptos_reactive/tests/memo.rs +++ b/leptos_reactive/tests/memo.rs @@ -212,3 +212,61 @@ fn dynamic_dependencies() { runtime.dispose(); } + +#[test] +fn raw_memo_slice() { + use std::rc::Rc; + let runtime = create_runtime(); + + // this could be serialized to and from localstorage with miniserde + pub struct State { + token: String, + dark_mode: bool, + } + + let state = create_rw_signal(State { + token: "".into(), + // this would cause flickering on reload, + // use a cookie for the initial value in real projects + dark_mode: false, + }); + + let token = create_raw_memo(move |old_token| { + state.with(move |state| { + if let Some(token) = old_token.filter(|old_token| old_token == &state.token) { + (token, false) + } else { + (state.token.clone(), true) + } + }) + }); + let set_token = + move |new_token| state.update(|state| state.token = new_token); + + let (_, set_dark_mode) = create_slice( + state, + |state| state.dark_mode, + |state, value| state.dark_mode = value, + ); + + let count_token_updates = Rc::new(std::cell::Cell::new(0)); + + assert_eq!(count_token_updates.get(), 0); + create_isomorphic_effect({ + let count_token_updates = Rc::clone(&count_token_updates); + move |_| { + token.track(); + count_token_updates.set(count_token_updates.get() + 1); + } + }); + assert_eq!(count_token_updates.get(), 1); + set_token("this is not a token!".into()); + // token was updated with the new token + token.with(|token| assert_eq!(token, "this is not a token!")); + assert_eq!(count_token_updates.get(), 2); + set_dark_mode.set(true); + // since token didn't change, there was also no update emitted + assert_eq!(count_token_updates.get(), 2); + + runtime.dispose(); +}