From 4a187e83f7af36e28738a1863bc038528dff863f Mon Sep 17 00:00:00 2001 From: Jose Quesada Date: Thu, 17 Nov 2022 18:44:10 -0600 Subject: [PATCH 1/6] impl `UntrackedGettableSignal` for `ReadSignal`, `RwSignal`, and `Memo` --- leptos_reactive/src/lib.rs | 16 ++++++++++++++++ leptos_reactive/src/memo.rs | 19 ++++++++++++++++++- leptos_reactive/src/signal.rs | 29 ++++++++++++++++++++++++++++- 3 files changed, 62 insertions(+), 2 deletions(-) diff --git a/leptos_reactive/src/lib.rs b/leptos_reactive/src/lib.rs index 87b2fd248..9985ed538 100644 --- a/leptos_reactive/src/lib.rs +++ b/leptos_reactive/src/lib.rs @@ -92,6 +92,22 @@ pub use signal::*; pub use spawn::*; pub use suspense::*; +/// Trait implemented for all signal types which you can `get` a value +/// from, such as [`ReadSignal`], +/// [`Memo`], etc., which allows getting the inner value without +/// subscribing to the current scope. +pub trait UntrackedGettableSignal { + /// Gets the signal's value without creating a dependency on the + /// current scope. + fn get_untracked(&self) -> T + where + T: Clone; + + /// Runs the provided closure with a reference to the current + /// value without creating a dependency on the current scope. + fn with_untracked(&self, f: impl FnOnce(&T) -> O) -> O; +} + #[doc(hidden)] #[macro_export] macro_rules! debug_warn { diff --git a/leptos_reactive/src/memo.rs b/leptos_reactive/src/memo.rs index 7c91b5fc5..cf3a21674 100644 --- a/leptos_reactive/src/memo.rs +++ b/leptos_reactive/src/memo.rs @@ -1,4 +1,4 @@ -use crate::{ReadSignal, Scope, SignalError}; +use crate::{ReadSignal, Scope, SignalError, UntrackedGettableSignal}; use std::fmt::Debug; /// Creates an efficient derived reactive value based on other reactive values. @@ -130,6 +130,23 @@ where impl Copy for Memo {} +impl UntrackedGettableSignal for Memo { + fn get_untracked(&self) -> T + where + T: Clone, + { + // Unwrapping is fine because `T` will already be `Some(T)` by + // the time this method can be called + self.0.get_untracked().unwrap() + } + + fn with_untracked(&self, f: impl FnOnce(&T) -> O) -> O { + // Unwrapping here is fine for the same reasons as ::get_untracked + self.0.with_untracked(|v| f(v.as_ref().unwrap())) + } +} + impl Memo where T: 'static, diff --git a/leptos_reactive/src/signal.rs b/leptos_reactive/src/signal.rs index dec2a29c0..e4e2726b4 100644 --- a/leptos_reactive/src/signal.rs +++ b/leptos_reactive/src/signal.rs @@ -1,4 +1,4 @@ -use crate::{debug_warn, spawn_local, Runtime, Scope, ScopeProperty}; +use crate::{debug_warn, spawn_local, Runtime, Scope, ScopeProperty, UntrackedGettableSignal}; use futures::Stream; use std::{fmt::Debug, marker::PhantomData}; use thiserror::Error; @@ -118,6 +118,19 @@ where pub(crate) ty: PhantomData, } +impl UntrackedGettableSignal for ReadSignal { + fn get_untracked(&self) -> T + where + T: Clone, + { + self.with_no_subscription(|v| v.clone()) + } + + fn with_untracked(&self, f: impl FnOnce(&T) -> O) -> O { + self.with_no_subscription(f) + } +} + impl ReadSignal where T: 'static, @@ -451,6 +464,20 @@ impl Clone for RwSignal { impl Copy for RwSignal {} +impl UntrackedGettableSignal for RwSignal { + fn get_untracked(&self) -> T + where + T: Clone, + { + self.id + .with_no_subscription(self.runtime, |v: &T| v.clone()) + } + + fn with_untracked(&self, f: impl FnOnce(&T) -> O) -> O { + self.id.with_no_subscription(self.runtime, f) + } +} + impl RwSignal where T: 'static, From 6e78e855900fd0b46a0f51926f0a577f30142d5f Mon Sep 17 00:00:00 2001 From: Jose Quesada Date: Thu, 17 Nov 2022 19:30:05 -0600 Subject: [PATCH 2/6] impl `UntrackedSettableSignal` for `WriteSignal`, `RwSignal` --- leptos_reactive/src/lib.rs | 13 +++++ leptos_reactive/src/signal.rs | 93 +++++++++++++++++++++++++---------- 2 files changed, 80 insertions(+), 26 deletions(-) diff --git a/leptos_reactive/src/lib.rs b/leptos_reactive/src/lib.rs index 9985ed538..c77143b88 100644 --- a/leptos_reactive/src/lib.rs +++ b/leptos_reactive/src/lib.rs @@ -108,6 +108,19 @@ pub trait UntrackedGettableSignal { fn with_untracked(&self, f: impl FnOnce(&T) -> O) -> O; } +/// Trait implemented for all signal types which you can `set` the inner +/// value, such as [`WriteSignal`] and [`RwSignal`], which allows setting +/// the inner value without causing effects which depend on the signal +/// from being run. +pub trait UntrackedSettableSignal { + /// Sets the signal's value without notifying dependents. + fn set(&self, new_value: T); + + /// Runs the provided closure with a mutable reference to the current + /// value without notifying dependents. + fn update_untracked(&self, f: impl FnOnce(&mut T)); +} + #[doc(hidden)] #[macro_export] macro_rules! debug_warn { diff --git a/leptos_reactive/src/signal.rs b/leptos_reactive/src/signal.rs index e4e2726b4..1d511cd24 100644 --- a/leptos_reactive/src/signal.rs +++ b/leptos_reactive/src/signal.rs @@ -1,4 +1,7 @@ -use crate::{debug_warn, spawn_local, Runtime, Scope, ScopeProperty, UntrackedGettableSignal}; +use crate::{ + debug_warn, spawn_local, Runtime, Scope, ScopeProperty, UntrackedGettableSignal, + UntrackedSettableSignal, +}; use futures::Stream; use std::{fmt::Debug, marker::PhantomData}; use thiserror::Error; @@ -298,6 +301,20 @@ where pub(crate) ty: PhantomData, } +impl UntrackedSettableSignal for WriteSignal +where + T: 'static, +{ + fn set(&self, new_value: T) { + self.id + .update_with_no_effect(self.runtime, |v| *v = new_value); + } + + fn update_untracked(&self, f: impl FnOnce(&mut T)) { + self.id.update_with_no_effect(self.runtime, f); + } +} + impl WriteSignal where T: 'static, @@ -478,6 +495,17 @@ impl UntrackedGettableSignal for RwSignal { } } +impl UntrackedSettableSignal for RwSignal { + fn set(&self, new_value: T) { + self.id + .update_with_no_effect(self.runtime, |v| *v = new_value); + } + + fn update_untracked(&self, f: impl FnOnce(&mut T)) { + self.id.update_with_no_effect(self.runtime, f); + } +} + impl RwSignal where T: 'static, @@ -714,36 +742,41 @@ impl SignalId { self.try_with(runtime, f).unwrap() } + fn update_value(&self, runtime: &Runtime, f: impl FnOnce(&mut T)) -> bool + where + T: 'static, + { + let value = { + let signals = runtime.signals.borrow(); + signals.get(*self).cloned() + }; + if let Some(value) = value { + let mut value = value.borrow_mut(); + if let Some(value) = value.downcast_mut::() { + f(value); + true + } else { + debug_warn!( + "[Signal::update] failed when downcasting to Signal<{}>", + std::any::type_name::() + ); + false + } + } else { + debug_warn!( + "[Signal::update] You’re trying to update a Signal<{}> that has already been disposed of. This is probably either a logic error in a component that creates and disposes of scopes, or a Resource resolving after its scope has been dropped without having been cleaned up.", + std::any::type_name::() + ); + false + } + } + pub(crate) fn update(&self, runtime: &Runtime, f: impl FnOnce(&mut T)) where T: 'static, { // update the value - let updated = { - let value = { - let signals = runtime.signals.borrow(); - signals.get(*self).cloned() - }; - if let Some(value) = value { - let mut value = value.borrow_mut(); - if let Some(value) = value.downcast_mut::() { - f(value); - true - } else { - debug_warn!( - "[Signal::update] failed when downcasting to Signal<{}>", - std::any::type_name::() - ); - false - } - } else { - debug_warn!( - "[Signal::update] You’re trying to update a Signal<{}> that has already been disposed of. This is probably either a logic error in a component that creates and disposes of scopes, or a Resource resolving after its scope has been dropped without having been cleaned up.", - std::any::type_name::() - ); - false - } - }; + let updated = self.update_value(runtime, f); // notify subscribers if updated { @@ -765,4 +798,12 @@ impl SignalId { } } } + + pub(crate) fn update_with_no_effect(&self, runtime: &Runtime, f: impl FnOnce(&mut T)) + where + T: 'static, + { + // update the value + self.update_value(runtime, f); + } } From fe41b6c840e679f00cdf1ec460e896865f187811 Mon Sep 17 00:00:00 2001 From: Jose Quesada Date: Fri, 18 Nov 2022 08:27:14 -0600 Subject: [PATCH 3/6] renamed `UntrackedSettableSignal::set` to `set_untracked` --- leptos_reactive/src/lib.rs | 2 +- leptos_reactive/src/signal.rs | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/leptos_reactive/src/lib.rs b/leptos_reactive/src/lib.rs index e1000e555..9d8603ca0 100644 --- a/leptos_reactive/src/lib.rs +++ b/leptos_reactive/src/lib.rs @@ -115,7 +115,7 @@ pub trait UntrackedGettableSignal { /// from being run. pub trait UntrackedSettableSignal { /// Sets the signal's value without notifying dependents. - fn set(&self, new_value: T); + fn set_untracked(&self, new_value: T); /// Runs the provided closure with a mutable reference to the current /// value without notifying dependents. diff --git a/leptos_reactive/src/signal.rs b/leptos_reactive/src/signal.rs index 165c3836b..6022bf311 100644 --- a/leptos_reactive/src/signal.rs +++ b/leptos_reactive/src/signal.rs @@ -303,7 +303,7 @@ impl UntrackedSettableSignal for WriteSignal where T: 'static, { - fn set(&self, new_value: T) { + fn set_untracked(&self, new_value: T) { self.id .update_with_no_effect(self.runtime, |v| *v = new_value); } @@ -494,7 +494,7 @@ impl UntrackedGettableSignal for RwSignal { } impl UntrackedSettableSignal for RwSignal { - fn set(&self, new_value: T) { + fn set_untracked(&self, new_value: T) { self.id .update_with_no_effect(self.runtime, |v| *v = new_value); } From 3d88227bacbc60d4f423e57893b58192074f49cc Mon Sep 17 00:00:00 2001 From: Jose Quesada Date: Fri, 18 Nov 2022 10:01:35 -0600 Subject: [PATCH 4/6] impl `UntrackedGettableSignal` for Signal --- leptos_reactive/src/signal_wrappers.rs | 55 ++++++++++++++++++++------ 1 file changed, 44 insertions(+), 11 deletions(-) diff --git a/leptos_reactive/src/signal_wrappers.rs b/leptos_reactive/src/signal_wrappers.rs index 24859ba59..270b38727 100644 --- a/leptos_reactive/src/signal_wrappers.rs +++ b/leptos_reactive/src/signal_wrappers.rs @@ -1,6 +1,6 @@ -use std::rc::Rc; +use std::{cell::RefCell, rc::Rc}; -use crate::{Memo, ReadSignal, RwSignal}; +use crate::{create_scope, Memo, ReadSignal, RwSignal, Scope, UntrackedGettableSignal}; /// A wrapper for any kind of readable reactive signal: a [ReadSignal](crate::ReadSignal), /// [Memo](crate::Memo), [RwSignal](crate::RwSignal), or derived signal closure. @@ -33,6 +33,39 @@ pub struct Signal(SignalTypes) where T: 'static; +/// Please note that using `Signal::with_untracked` still clones the inner value, +/// so there's no benefit to using it as opposed to calling +/// `Signal::get_untracked`. +impl UntrackedGettableSignal for Signal +where + T: 'static, +{ + fn get_untracked(&self) -> T + where + T: Clone, + { + match &self.0 { + SignalTypes::ReadSignal(s) => s.get_untracked(), + SignalTypes::Memo(m) => m.get_untracked(), + SignalTypes::DerivedSignal(cx, f) => cx.untrack(|| f()), + } + } + + fn with_untracked(&self, f: impl FnOnce(&T) -> O) -> O { + match &self.0 { + SignalTypes::ReadSignal(s) => s.with_untracked(f), + SignalTypes::Memo(s) => s.with_untracked(f), + SignalTypes::DerivedSignal(cx, v_f) => { + let mut o = None; + + cx.untrack(|| o = Some(f(&v_f()))); + + o.unwrap() + } + } + } +} + impl Signal where T: 'static, @@ -54,8 +87,8 @@ where /// assert_eq!(above_3(&double_count), true); /// # }); /// ``` - pub fn derive(derived_signal: impl Fn() -> T + 'static) -> Self { - Self(SignalTypes::DerivedSignal(Rc::new(derived_signal))) + pub fn derive(cx: Scope, derived_signal: impl Fn() -> T + 'static) -> Self { + Self(SignalTypes::DerivedSignal(cx, Rc::new(derived_signal))) } /// Applies a function to the current value of the signal, and subscribes @@ -91,7 +124,7 @@ where match &self.0 { SignalTypes::ReadSignal(s) => s.with(f), SignalTypes::Memo(s) => s.with(f), - SignalTypes::DerivedSignal(s) => f(&s()), + SignalTypes::DerivedSignal(_, s) => f(&s()), } } @@ -124,7 +157,7 @@ where match &self.0 { SignalTypes::ReadSignal(s) => s.get(), SignalTypes::Memo(s) => s.get(), - SignalTypes::DerivedSignal(s) => s(), + SignalTypes::DerivedSignal(_, s) => s(), } } } @@ -154,7 +187,7 @@ where { ReadSignal(ReadSignal), Memo(Memo), - DerivedSignal(Rc T>), + DerivedSignal(Scope, Rc T>), } impl std::fmt::Debug for SignalTypes @@ -165,7 +198,7 @@ where match self { Self::ReadSignal(arg0) => f.debug_tuple("ReadSignal").field(arg0).finish(), Self::Memo(arg0) => f.debug_tuple("Memo").field(arg0).finish(), - Self::DerivedSignal(_) => f.debug_tuple("DerivedSignal").finish(), + Self::DerivedSignal(_, _) => f.debug_tuple("DerivedSignal").finish(), } } } @@ -178,7 +211,7 @@ where match (self, other) { (Self::ReadSignal(l0), Self::ReadSignal(r0)) => l0 == r0, (Self::Memo(l0), Self::Memo(r0)) => l0 == r0, - (Self::DerivedSignal(l0), Self::DerivedSignal(r0)) => std::ptr::eq(l0, r0), + (Self::DerivedSignal(_, l0), Self::DerivedSignal(_, r0)) => std::ptr::eq(l0, r0), _ => false, } } @@ -276,8 +309,8 @@ where /// assert_eq!(above_3(&double_count), true); /// # }); /// ``` - pub fn derive(derived_signal: impl Fn() -> T + 'static) -> Self { - Self::Dynamic(Signal::derive(derived_signal)) + pub fn derive(cx: Scope, derived_signal: impl Fn() -> T + 'static) -> Self { + Self::Dynamic(Signal::derive(cx, derived_signal)) } /// Applies a function to the current value of the signal, and subscribes From 00b6b39ee01895cceaa94033faedfa02654cc72a Mon Sep 17 00:00:00 2001 From: Jose Quesada Date: Fri, 18 Nov 2022 10:08:28 -0600 Subject: [PATCH 5/6] impl `UntrackedGettableSignal` for `MaybeSignal` --- leptos_reactive/src/signal_wrappers.rs | 22 ++++++++++++++++++++++ 1 file changed, 22 insertions(+) diff --git a/leptos_reactive/src/signal_wrappers.rs b/leptos_reactive/src/signal_wrappers.rs index 270b38727..dc457e4a3 100644 --- a/leptos_reactive/src/signal_wrappers.rs +++ b/leptos_reactive/src/signal_wrappers.rs @@ -288,6 +288,28 @@ where Dynamic(Signal), } +impl UntrackedGettableSignal for MaybeSignal +where + T: 'static, +{ + fn get_untracked(&self) -> T + where + T: Clone, + { + match self { + Self::Static(t) => t.clone(), + Self::Dynamic(s) => s.get_untracked(), + } + } + + fn with_untracked(&self, f: impl FnOnce(&T) -> O) -> O { + match self { + Self::Static(t) => f(t), + Self::Dynamic(s) => s.with_untracked(f), + } + } +} + impl MaybeSignal where T: 'static, From 5562e2d6ee5f3b50566b1e18d2d959ae883e9123 Mon Sep 17 00:00:00 2001 From: Greg Johnston Date: Fri, 18 Nov 2022 11:30:26 -0500 Subject: [PATCH 6/6] Tests --- leptos_reactive/src/signal_wrappers.rs | 16 +++--- leptos_reactive/tests/untracked.rs | 79 ++++++++++++++++++++++++++ 2 files changed, 87 insertions(+), 8 deletions(-) create mode 100644 leptos_reactive/tests/untracked.rs diff --git a/leptos_reactive/src/signal_wrappers.rs b/leptos_reactive/src/signal_wrappers.rs index dc457e4a3..fd60baa2c 100644 --- a/leptos_reactive/src/signal_wrappers.rs +++ b/leptos_reactive/src/signal_wrappers.rs @@ -13,7 +13,7 @@ use crate::{create_scope, Memo, ReadSignal, RwSignal, Scope, UntrackedGettableSi /// # use leptos_reactive::{create_scope, create_signal, create_rw_signal, create_memo, Signal}; /// # create_scope(|cx| { /// let (count, set_count) = create_signal(cx, 2); -/// let double_count = Signal::derive(move || count() * 2); +/// let double_count = Signal::derive(cx, move || count() * 2); /// let memoized_double_count = create_memo(cx, move |_| count() * 2); /// /// // this function takes any kind of wrapped signal @@ -76,7 +76,7 @@ where /// # use leptos_reactive::{create_scope, create_signal, create_rw_signal, create_memo, Signal}; /// # create_scope(|cx| { /// let (count, set_count) = create_signal(cx, 2); - /// let double_count = Signal::derive(move || count() * 2); + /// let double_count = Signal::derive(cx, move || count() * 2); /// /// // this function takes any kind of wrapped signal /// fn above_3(arg: &Signal) -> bool { @@ -97,7 +97,7 @@ where /// # use leptos_reactive::*; /// # create_scope(|cx| { /// let (name, set_name) = create_signal(cx, "Alice".to_string()); - /// let name_upper = Signal::derive(move || name.with(|n| n.to_uppercase())); + /// let name_upper = Signal::derive(cx, move || name.with(|n| n.to_uppercase())); /// let memoized_lower = create_memo(cx, move |_| name.with(|n| n.to_lowercase())); /// /// // this function takes any kind of wrapped signal @@ -137,7 +137,7 @@ where /// # use leptos_reactive::{create_scope, create_signal, create_rw_signal, create_memo, Signal}; /// # create_scope(|cx| { /// let (count, set_count) = create_signal(cx, 2); - /// let double_count = Signal::derive(move || count() * 2); + /// let double_count = Signal::derive(cx, move || count() * 2); /// let memoized_double_count = create_memo(cx, move |_| count() * 2); /// /// // this function takes any kind of wrapped signal @@ -260,7 +260,7 @@ where /// # use leptos_reactive::*; /// # create_scope(|cx| { /// let (count, set_count) = create_signal(cx, 2); -/// let double_count = MaybeSignal::derive(move || count() * 2); +/// let double_count = MaybeSignal::derive(cx, move || count() * 2); /// let memoized_double_count = create_memo(cx, move |_| count() * 2); /// let static_value = 5; /// @@ -320,7 +320,7 @@ where /// # use leptos_reactive::{create_scope, create_signal, create_rw_signal, create_memo, Signal}; /// # create_scope(|cx| { /// let (count, set_count) = create_signal(cx, 2); - /// let double_count = Signal::derive(move || count() * 2); + /// let double_count = Signal::derive(cx, move || count() * 2); /// /// // this function takes any kind of wrapped signal /// fn above_3(arg: &Signal) -> bool { @@ -341,7 +341,7 @@ where /// # use leptos_reactive::*; /// # create_scope(|cx| { /// let (name, set_name) = create_signal(cx, "Alice".to_string()); - /// let name_upper = MaybeSignal::derive(move || name.with(|n| n.to_uppercase())); + /// let name_upper = MaybeSignal::derive(cx, move || name.with(|n| n.to_uppercase())); /// let memoized_lower = create_memo(cx, move |_| name.with(|n| n.to_lowercase())); /// let static_value: MaybeSignal = "Bob".to_string().into(); /// @@ -383,7 +383,7 @@ where /// # use leptos_reactive::*; /// # create_scope(|cx| { /// let (count, set_count) = create_signal(cx, 2); - /// let double_count = MaybeSignal::derive(move || count() * 2); + /// let double_count = MaybeSignal::derive(cx, move || count() * 2); /// let memoized_double_count = create_memo(cx, move |_| count() * 2); /// let static_value: MaybeSignal = 5.into(); /// diff --git a/leptos_reactive/tests/untracked.rs b/leptos_reactive/tests/untracked.rs new file mode 100644 index 000000000..10b63feb8 --- /dev/null +++ b/leptos_reactive/tests/untracked.rs @@ -0,0 +1,79 @@ +//#[cfg(not(feature = "stable"))] +use leptos_reactive::{ + create_isomorphic_effect, create_scope, create_signal, UntrackedGettableSignal, + UntrackedSettableSignal, +}; + +//#[cfg(not(feature = "stable"))] +#[test] +fn untracked_set_doesnt_trigger_effect() { + use std::cell::RefCell; + use std::rc::Rc; + + create_scope(|cx| { + let (a, set_a) = create_signal(cx, -1); + + // simulate an arbitrary side effect + let b = Rc::new(RefCell::new(String::new())); + + create_isomorphic_effect(cx, { + let b = b.clone(); + move |_| { + let formatted = format!("Value is {}", a()); + *b.borrow_mut() = formatted; + } + }); + + assert_eq!(b.borrow().as_str(), "Value is -1"); + + set_a.set(1); + + assert_eq!(b.borrow().as_str(), "Value is 1"); + + set_a.set_untracked(-1); + + assert_eq!(b.borrow().as_str(), "Value is 1"); + }) + .dispose() +} + +#[test] +fn untracked_get_doesnt_trigger_effect() { + use std::cell::RefCell; + use std::rc::Rc; + + create_scope(|cx| { + let (a, set_a) = create_signal(cx, -1); + let (a2, set_a2) = create_signal(cx, 1); + + // simulate an arbitrary side effect + let b = Rc::new(RefCell::new(String::new())); + + create_isomorphic_effect(cx, { + let b = b.clone(); + move |_| { + let formatted = format!("Values are {} and {}", a(), a2.get_untracked()); + *b.borrow_mut() = formatted; + } + }); + + assert_eq!(b.borrow().as_str(), "Values are -1 and 1"); + + set_a.set(1); + + assert_eq!(b.borrow().as_str(), "Values are 1 and 1"); + + set_a.set_untracked(-1); + + assert_eq!(b.borrow().as_str(), "Values are 1 and 1"); + + set_a2.set(-1); + + assert_eq!(b.borrow().as_str(), "Values are 1 and 1"); + + set_a.set(-1); + + assert_eq!(b.borrow().as_str(), "Values are -1 and -1"); + }) + .dispose() +}