diff --git a/packages/signals/Cargo.toml b/packages/signals/Cargo.toml index 3df7d34ca..2c24758b5 100644 --- a/packages/signals/Cargo.toml +++ b/packages/signals/Cargo.toml @@ -15,3 +15,4 @@ simple_logger = "4.2.0" [dev-dependencies] dioxus = { workspace = true } dioxus-desktop = { workspace = true } +tokio = { version = "1", features = ["full"] } diff --git a/packages/signals/examples/dependancies.rs b/packages/signals/examples/dependancies.rs new file mode 100644 index 000000000..7b744eb3c --- /dev/null +++ b/packages/signals/examples/dependancies.rs @@ -0,0 +1,38 @@ +#![allow(non_snake_case)] + +use dioxus::prelude::*; +use dioxus_signals::*; + +fn main() { + dioxus_desktop::launch(app); +} + +fn app(cx: Scope) -> Element { + let signal = use_signal(cx, || 0); + + use_future!(cx, || async move { + loop { + tokio::time::sleep(std::time::Duration::from_secs(1)).await; + *signal.write() += 1; + } + }); + + let local_state = use_state(cx, || 0); + let computed = + use_selector_with_dependencies(cx, (local_state.get(),), move |(local_state,)| { + local_state * 2 + signal.value() + }); + println!("Running app"); + + render! { + button { + onclick: move |_| { + local_state.set(local_state.get() + 1); + }, + "Add one" + } + div { + "{computed}" + } + } +} diff --git a/packages/signals/src/dependency.rs b/packages/signals/src/dependency.rs new file mode 100644 index 000000000..d6ab65a5f --- /dev/null +++ b/packages/signals/src/dependency.rs @@ -0,0 +1,67 @@ +/// A dependency is a trait that can be used to determine if a effect or selector should be re-run. +pub trait Dependency: Sized + Clone { + /// The output of the dependency + type Out: Clone + PartialEq; + /// Returns the output of the dependency. + fn out(&self) -> Self::Out; + /// Returns true if the dependency has changed. + fn changed(&self, other: &Self::Out) -> bool { + self.out() != *other + } +} + +impl Dependency for () { + type Out = (); + fn out(&self) -> Self::Out {} +} + +/// A dependency is a trait that can be used to determine if a effect or selector should be re-run. +pub trait Dep: 'static + PartialEq + Clone {} +impl Dep for T where T: 'static + PartialEq + Clone {} + +impl Dependency for &A { + type Out = A; + fn out(&self) -> Self::Out { + (*self).clone() + } +} + +macro_rules! impl_dep { + ( + $($el:ident=$name:ident $other:ident,)* + ) => { + impl< $($el),* > Dependency for ($(&$el,)*) + where + $( + $el: Dep + ),* + { + type Out = ($($el,)*); + + fn out(&self) -> Self::Out { + let ($($name,)*) = self; + ($((*$name).clone(),)*) + } + + fn changed(&self, other: &Self::Out) -> bool { + let ($($name,)*) = self; + let ($($other,)*) = other; + $( + if *$name != $other { + return true; + } + )* + false + } + } + }; +} + +impl_dep!(A = a1 a2,); +impl_dep!(A = a1 a2, B = b1 b2,); +impl_dep!(A = a1 a2, B = b1 b2, C = c1 c2,); +impl_dep!(A = a1 a2, B = b1 b2, C = c1 c2, D = d1 d2,); +impl_dep!(A = a1 a2, B = b1 b2, C = c1 c2, D = d1 d2, E = e1 e2,); +impl_dep!(A = a1 a2, B = b1 b2, C = c1 c2, D = d1 d2, E = e1 e2, F = f1 f2,); +impl_dep!(A = a1 a2, B = b1 b2, C = c1 c2, D = d1 d2, E = e1 e2, F = f1 f2, G = g1 g2,); +impl_dep!(A = a1 a2, B = b1 b2, C = c1 c2, D = d1 d2, E = e1 e2, F = f1 f2, G = g1 g2, H = h1 h2,); diff --git a/packages/signals/src/effect.rs b/packages/signals/src/effect.rs index be57f5cfd..fdceb5211 100644 --- a/packages/signals/src/effect.rs +++ b/packages/signals/src/effect.rs @@ -1,9 +1,12 @@ use core::{self, fmt::Debug}; use std::fmt::{self, Formatter}; +use std::marker::PhantomData; use dioxus_core::prelude::*; -use crate::CopyValue; +use crate::dependency::Dep; +use crate::{dependency::Dependency, CopyValue}; +use crate::{use_signal, Signal}; #[derive(Default, Clone, Copy)] pub(crate) struct EffectStack { @@ -26,6 +29,28 @@ pub fn use_effect(cx: &ScopeState, callback: impl FnMut() + 'static) { cx.use_hook(|| Effect::new(callback)); } +/// Create a new effect. The effect will be run immediately and whenever any signal it reads changes. +/// The signal will be owned by the current component and will be dropped when the component is dropped. +pub fn use_effect_with_dependencies( + cx: &ScopeState, + dependencies: D, + mut callback: impl FnMut(D::Out) + 'static, +) where + D::Out: 'static, +{ + let dependencies_signal = use_signal(cx, || dependencies.out()); + cx.use_hook(|| { + Effect::new(move || { + let deref = &*dependencies_signal.read(); + callback(deref.clone()); + }); + }); + let changed = { dependencies.changed(&*dependencies_signal.read()) }; + if changed { + dependencies_signal.set(dependencies.out()); + } +} + /// Effects allow you to run code when a signal changes. Effects are run immediately and whenever any signal it reads changes. #[derive(Copy, Clone, PartialEq)] pub struct Effect { diff --git a/packages/signals/src/lib.rs b/packages/signals/src/lib.rs index 1c67ab462..1f8bc3297 100644 --- a/packages/signals/src/lib.rs +++ b/packages/signals/src/lib.rs @@ -10,3 +10,5 @@ mod selector; pub use selector::*; pub(crate) mod signal; pub use signal::*; +mod dependency; +pub use dependency::*; diff --git a/packages/signals/src/selector.rs b/packages/signals/src/selector.rs index 79a8f679b..81fb50398 100644 --- a/packages/signals/src/selector.rs +++ b/packages/signals/src/selector.rs @@ -1,5 +1,7 @@ use dioxus_core::prelude::*; +use crate::dependency::Dependency; +use crate::use_signal; use crate::{get_effect_stack, signal::SignalData, CopyValue, Effect, ReadOnlySignal, Signal}; /// Creates a new Selector. The selector will be run immediately and whenever any signal it reads changes. @@ -26,6 +28,44 @@ pub fn use_selector( *cx.use_hook(|| selector(f)) } +/// Creates a new Selector with some local dependencies. The selector will be run immediately and whenever any signal it reads or any dependencies it tracks changes +/// +/// Selectors can be used to efficiently compute derived data from signals. +/// +/// ```rust +/// use dioxus::prelude::*; +/// use dioxus_signals::*; +/// +/// fn App(cx: Scope) -> Element { +/// let mut local_state = use_state(cx, || 0); +/// let double = use_selector_with_dependencies(cx, (local_state.get(),), move |(local_state,)| local_state * 2); +/// local_state.set(1); +/// +/// render! { "{double}" } +/// } +/// ``` +pub fn use_selector_with_dependencies( + cx: &ScopeState, + dependencies: D, + mut f: impl FnMut(D::Out) -> R + 'static, +) -> ReadOnlySignal +where + D::Out: 'static, +{ + let dependencies_signal = use_signal(cx, || dependencies.out()); + let selector = *cx.use_hook(|| { + selector(move || { + let deref = &*dependencies_signal.read(); + f(deref.clone()) + }) + }); + let changed = { dependencies.changed(&*dependencies_signal.read()) }; + if changed { + dependencies_signal.set(dependencies.out()); + } + selector +} + /// Creates a new Selector. The selector will be run immediately and whenever any signal it reads changes. /// /// Selectors can be used to efficiently compute derived data from signals.