Merge pull request #89 from jquesada2016/main

Add ability to get/set signals untracked
This commit is contained in:
Greg Johnston 2022-11-18 12:13:07 -05:00 committed by GitHub
commit 491f124669
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
5 changed files with 294 additions and 46 deletions

View File

@ -93,6 +93,35 @@ pub use signal_wrappers::*;
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<T> {
/// 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<O>(&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<T> {
/// Sets the signal's value without notifying dependents.
fn set_untracked(&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 {

View File

@ -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<T> Copy for Memo<T> {}
impl<T> UntrackedGettableSignal<T> for Memo<T> {
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<O>(&self, f: impl FnOnce(&T) -> O) -> O {
// Unwrapping here is fine for the same reasons as <Memo as
// UntrackedSignal>::get_untracked
self.0.with_untracked(|v| f(v.as_ref().unwrap()))
}
}
impl<T> Memo<T>
where
T: 'static,

View File

@ -1,4 +1,7 @@
use crate::{debug_warn, spawn_local, Runtime, Scope, ScopeProperty};
use crate::{
debug_warn, spawn_local, Runtime, Scope, ScopeProperty, UntrackedGettableSignal,
UntrackedSettableSignal,
};
use futures::Stream;
use std::{fmt::Debug, marker::PhantomData};
use thiserror::Error;
@ -118,6 +121,19 @@ where
pub(crate) ty: PhantomData<T>,
}
impl<T> UntrackedGettableSignal<T> for ReadSignal<T> {
fn get_untracked(&self) -> T
where
T: Clone,
{
self.with_no_subscription(|v| v.clone())
}
fn with_untracked<O>(&self, f: impl FnOnce(&T) -> O) -> O {
self.with_no_subscription(f)
}
}
impl<T> ReadSignal<T>
where
T: 'static,
@ -283,6 +299,20 @@ where
pub(crate) ty: PhantomData<T>,
}
impl<T> UntrackedSettableSignal<T> for WriteSignal<T>
where
T: 'static,
{
fn set_untracked(&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<T> WriteSignal<T>
where
T: 'static,
@ -449,6 +479,31 @@ impl<T> Clone for RwSignal<T> {
impl<T> Copy for RwSignal<T> {}
impl<T> UntrackedGettableSignal<T> for RwSignal<T> {
fn get_untracked(&self) -> T
where
T: Clone,
{
self.id
.with_no_subscription(self.runtime, |v: &T| v.clone())
}
fn with_untracked<O>(&self, f: impl FnOnce(&T) -> O) -> O {
self.id.with_no_subscription(self.runtime, f)
}
}
impl<T> UntrackedSettableSignal<T> for RwSignal<T> {
fn set_untracked(&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<T> RwSignal<T>
where
T: 'static,
@ -685,36 +740,41 @@ impl SignalId {
self.try_with(runtime, f).unwrap()
}
fn update_value<T>(&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::<T>() {
f(value);
true
} else {
debug_warn!(
"[Signal::update] failed when downcasting to Signal<{}>",
std::any::type_name::<T>()
);
false
}
} else {
debug_warn!(
"[Signal::update] Youre 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::<T>()
);
false
}
}
pub(crate) fn update<T>(&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::<T>() {
f(value);
true
} else {
debug_warn!(
"[Signal::update] failed when downcasting to Signal<{}>",
std::any::type_name::<T>()
);
false
}
} else {
debug_warn!(
"[Signal::update] Youre 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::<T>()
);
false
}
};
let updated = self.update_value(runtime, f);
// notify subscribers
if updated {
@ -736,4 +796,12 @@ impl SignalId {
}
}
}
pub(crate) fn update_with_no_effect<T>(&self, runtime: &Runtime, f: impl FnOnce(&mut T))
where
T: 'static,
{
// update the value
self.update_value(runtime, f);
}
}

View File

@ -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.
@ -13,7 +13,7 @@ use crate::{Memo, ReadSignal, RwSignal};
/// # 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
@ -33,6 +33,39 @@ pub struct Signal<T>(SignalTypes<T>)
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<T> UntrackedGettableSignal<T> for Signal<T>
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<O>(&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<T> Signal<T>
where
T: 'static,
@ -43,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<i32>) -> bool {
@ -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
@ -64,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
@ -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()),
}
}
@ -104,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
@ -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<T>),
Memo(Memo<T>),
DerivedSignal(Rc<dyn Fn() -> T>),
DerivedSignal(Scope, Rc<dyn Fn() -> T>),
}
impl<T> std::fmt::Debug for SignalTypes<T>
@ -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,
}
}
@ -227,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;
///
@ -255,6 +288,28 @@ where
Dynamic(Signal<T>),
}
impl<T> UntrackedGettableSignal<T> for MaybeSignal<T>
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<O>(&self, f: impl FnOnce(&T) -> O) -> O {
match self {
Self::Static(t) => f(t),
Self::Dynamic(s) => s.with_untracked(f),
}
}
}
impl<T> MaybeSignal<T>
where
T: 'static,
@ -265,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<i32>) -> bool {
@ -276,8 +331,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
@ -286,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<String> = "Bob".to_string().into();
///
@ -328,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<i32> = 5.into();
///

View File

@ -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()
}