Skip to content

Commit

Permalink
feat: Implement "raw" memo to allow memos that re-use the previous value
Browse files Browse the repository at this point in the history
  • Loading branch information
pheki committed Dec 29, 2023
1 parent 15946c6 commit 37e4896
Show file tree
Hide file tree
Showing 3 changed files with 120 additions and 23 deletions.
79 changes: 59 additions & 20 deletions leptos_reactive/src/memo.rs
Original file line number Diff line number Diff line change
Expand Up @@ -86,7 +86,33 @@ pub fn create_memo<T>(f: impl Fn(Option<&T>) -> T + 'static) -> Memo<T>
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::<T>()
)
)
)]
#[track_caller]
#[inline(always)]
pub fn create_raw_memo<T>(
f: impl Fn(Option<T>) -> (T, bool) + 'static,
) -> Memo<T>
where
T: PartialEq + 'static,
{
Runtime::current().create_raw_memo(f)
}

/// An efficient derived reactive value based on other reactive values.
Expand Down Expand Up @@ -216,6 +242,16 @@ impl<T> Memo<T> {
{
create_memo(f)
}

#[allow(missing_docs)] // TODO
#[inline(always)]
#[track_caller]
pub fn new_raw(f: impl Fn(Option<T>) -> (T, bool) + 'static) -> Memo<T>
where
T: PartialEq + 'static,
{
create_raw_memo(f)
}
}

impl<T> Clone for Memo<T>
Expand Down Expand Up @@ -519,8 +555,8 @@ impl_get_fn_traits![Memo];

pub(crate) struct MemoState<T, F>
where
T: PartialEq + 'static,
F: Fn(Option<&T>) -> T,
T: 'static,
F: Fn(Option<T>) -> (T, bool),
{
pub f: F,
pub t: PhantomData<T>,
Expand All @@ -530,8 +566,8 @@ where

impl<T, F> AnyComputation for MemoState<T, F>
where
T: PartialEq + 'static,
F: Fn(Option<&T>) -> T,
T: 'static,
F: Fn(Option<T>) -> (T, bool),
{
#[cfg_attr(
any(debug_assertions, feature = "ssr"),
Expand All @@ -546,24 +582,27 @@ where
)
)]
fn run(&self, value: Rc<RefCell<dyn Any>>) -> bool {
let (new_value, is_different) = {
let value = value.borrow();
let curr_value = value
.downcast_ref::<Option<T>>()
.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::<Option<T>>()
.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::<Option<T>>()
.expect("to downcast memo value");
*value = Some(new_value);

is_different
}
Expand Down
6 changes: 3 additions & 3 deletions leptos_reactive/src/runtime.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1191,12 +1191,12 @@ impl RuntimeId {

#[track_caller]
#[inline(always)]
pub(crate) fn create_memo<T>(
pub(crate) fn create_raw_memo<T>(
self,
f: impl Fn(Option<&T>) -> T + 'static,
f: impl Fn(Option<T>) -> (T, bool) + 'static,
) -> Memo<T>
where
T: PartialEq + Any + 'static,
T: 'static,
{
Memo {
id: self.create_concrete_memo(
Expand Down
58 changes: 58 additions & 0 deletions leptos_reactive/tests/memo.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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();
}

0 comments on commit 37e4896

Please sign in to comment.