Merge pull request #89 from jquesada2016/main
Add ability to get/set signals untracked
This commit is contained in:
commit
491f124669
|
@ -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 {
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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] 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::<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] 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::<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);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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();
|
||||
///
|
||||
|
|
|
@ -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()
|
||||
}
|
Loading…
Reference in New Issue