From d815c7cc8980c1b8592c131314af3b63867347c0 Mon Sep 17 00:00:00 2001 From: Evan Almloff Date: Tue, 31 Oct 2023 18:30:06 -0500 Subject: [PATCH 1/2] create a reactive selector called comparer --- packages/signals/Cargo.toml | 3 +- packages/signals/src/comparer.rs | 109 +++++++++++++++++++++++++++++++ packages/signals/src/lib.rs | 2 + packages/signals/src/signal.rs | 6 ++ 4 files changed, 119 insertions(+), 1 deletion(-) create mode 100644 packages/signals/src/comparer.rs diff --git a/packages/signals/Cargo.toml b/packages/signals/Cargo.toml index 56e19bbd8..f78487c8c 100644 --- a/packages/signals/Cargo.toml +++ b/packages/signals/Cargo.toml @@ -12,6 +12,7 @@ generational-box = { workspace = true } tracing = { workspace = true } simple_logger = "4.2.0" serde = { version = "1", features = ["derive"], optional = true } +rustc-hash.workspace = true [dev-dependencies] dioxus = { workspace = true } @@ -20,4 +21,4 @@ tokio = { version = "1", features = ["full"] } [features] default = [] -serialize = ["serde"] \ No newline at end of file +serialize = ["serde"] diff --git a/packages/signals/src/comparer.rs b/packages/signals/src/comparer.rs new file mode 100644 index 000000000..3fea11cf5 --- /dev/null +++ b/packages/signals/src/comparer.rs @@ -0,0 +1,109 @@ +use std::hash::Hash; + +use dioxus_core::prelude::*; + +use crate::{CopyValue, Effect, ReadOnlySignal, Signal}; +use rustc_hash::FxHashMap; + +/// An object that can efficiently compare a value to a set of values. +#[derive(Debug)] +pub struct Comparer { + subscribers: CopyValue>>, +} + +impl Comparer { + /// Returns a signal which is true when the value is equal to the value passed to this function. + pub fn equal(&self, value: R) -> ReadOnlySignal { + let subscribers = self.subscribers.read(); + + match subscribers.get(&value) { + Some(&signal) => signal.into(), + None => { + drop(subscribers); + let mut subscribers = self.subscribers.write(); + let signal = Signal::new(false); + subscribers.insert(value, signal.clone()); + signal.into() + } + } + } +} + +impl Clone for Comparer { + fn clone(&self) -> Self { + Self { + subscribers: self.subscribers.clone(), + } + } +} + +impl Copy for Comparer {} + +/// Creates a new Comparer which efficiently tracks when a value changes to check if it is equal to a set of values. +/// +/// Generally, you shouldn't need to use this hook. Instead you can use [`crate::use_selector`]. If you have many values that you need to compare to a single value, this hook will change updates from O(n) to O(1) where n is the number of values you are comparing to. +/// +/// ```rust +/// use dioxus::prelude::*; +/// use dioxus_signals::*; +/// +/// fn App(cx: Scope) -> Element { +/// let mut count = use_signal(cx, || 0); +/// let comparer = use_comparer(cx, move || count.value()); +/// +/// render! { +/// for i in 0..10 { +/// // Child will only re-render when i == count +/// Child { active: comparer.equal(i) } +/// } +/// button { +/// // This will only rerender the child with the old and new value of i == count +/// // Because we are using a comparer, this will be O(1) instead of the O(n) performance of a selector +/// onclick: move |_| count += 1, +/// "Increment" +/// } +/// } +/// } +/// +/// #[component] +/// fn Child(cx: Scope, active: ReadOnlySignal) -> Element { +/// if *active() { +/// render! { "Active" } +/// } else { +/// render! { "Inactive" } +/// } +/// } +/// ``` +#[must_use] +pub fn use_comparer(cx: &ScopeState, f: impl FnMut() -> R + 'static) -> Comparer { + *cx.use_hook(move || comparer(f)) +} + +/// Creates a new Comparer which efficiently tracks when a value changes to check if it is equal to a set of values. +/// +/// Generally, you shouldn't need to use this hook. Instead you can use [`crate::use_selector`]. If you have many values that you need to compare to a single value, this hook will change updates from O(n) to O(1) where n is the number of values you are comparing to. +pub fn comparer(mut f: impl FnMut() -> R + 'static) -> Comparer { + let subscribers: CopyValue>> = CopyValue::new(FxHashMap::default()); + let previous = CopyValue::new(None); + + Effect::new(move || { + let subscribers = subscribers.read(); + let mut previous = previous.write(); + + if let Some(previous) = previous.take() { + if let Some(value) = subscribers.get(&previous) { + value.set(false); + } + } + + let current = f(); + + if let Some(value) = subscribers.get(¤t) { + value.set(true); + } + + *previous = Some(current); + }); + + Comparer { subscribers } +} diff --git a/packages/signals/src/lib.rs b/packages/signals/src/lib.rs index dce9af0a8..5ee243afd 100644 --- a/packages/signals/src/lib.rs +++ b/packages/signals/src/lib.rs @@ -14,3 +14,5 @@ pub(crate) mod signal; pub use signal::*; mod dependency; pub use dependency::*; +mod comparer; +pub use comparer::*; diff --git a/packages/signals/src/signal.rs b/packages/signals/src/signal.rs index 9213307ff..fb7c7fa6a 100644 --- a/packages/signals/src/signal.rs +++ b/packages/signals/src/signal.rs @@ -369,6 +369,12 @@ pub struct ReadOnlySignal { inner: Signal, } +impl From> for ReadOnlySignal { + fn from(inner: Signal) -> Self { + Self { inner } + } +} + impl ReadOnlySignal { /// Create a new read-only signal. pub fn new(signal: Signal) -> Self { From ff263808389c4b43ded90ebf166add08f9813852 Mon Sep 17 00:00:00 2001 From: Evan Almloff Date: Wed, 1 Nov 2023 08:21:04 -0500 Subject: [PATCH 2/2] fix clippy --- packages/core/src/diff.rs | 2 +- packages/fermi/src/lib.rs | 2 -- packages/rsx/src/lib.rs | 2 +- packages/signals/src/comparer.rs | 6 ++---- 4 files changed, 4 insertions(+), 8 deletions(-) diff --git a/packages/core/src/diff.rs b/packages/core/src/diff.rs index 74017db0f..2de862218 100644 --- a/packages/core/src/diff.rs +++ b/packages/core/src/diff.rs @@ -560,7 +560,7 @@ impl<'b> VirtualDom { // If none of the old keys are reused by the new children, then we remove all the remaining old children and // create the new children afresh. if shared_keys.is_empty() { - if old.get(0).is_some() { + if old.first().is_some() { self.remove_nodes(&old[1..]); self.replace(&old[0], new); } else { diff --git a/packages/fermi/src/lib.rs b/packages/fermi/src/lib.rs index 52bf4cd09..dc5ccf010 100644 --- a/packages/fermi/src/lib.rs +++ b/packages/fermi/src/lib.rs @@ -22,8 +22,6 @@ mod atoms { pub use atom::*; pub use atomfamily::*; pub use atomref::*; - pub use selector::*; - pub use selectorfamily::*; } pub mod hooks { diff --git a/packages/rsx/src/lib.rs b/packages/rsx/src/lib.rs index bdf32b272..5db6baee9 100644 --- a/packages/rsx/src/lib.rs +++ b/packages/rsx/src/lib.rs @@ -193,7 +193,7 @@ impl<'a> ToTokens for TemplateRenderer<'a> { fn to_tokens(&self, out_tokens: &mut TokenStream2) { let mut context = DynamicContext::default(); - let key = match self.roots.get(0) { + let key = match self.roots.first() { Some(BodyNode::Element(el)) if self.roots.len() == 1 => el.key.clone(), Some(BodyNode::Component(comp)) if self.roots.len() == 1 => comp.key().cloned(), _ => None, diff --git a/packages/signals/src/comparer.rs b/packages/signals/src/comparer.rs index 3fea11cf5..e55654ced 100644 --- a/packages/signals/src/comparer.rs +++ b/packages/signals/src/comparer.rs @@ -22,7 +22,7 @@ impl Comparer { drop(subscribers); let mut subscribers = self.subscribers.write(); let signal = Signal::new(false); - subscribers.insert(value, signal.clone()); + subscribers.insert(value, signal); signal.into() } } @@ -31,9 +31,7 @@ impl Comparer { impl Clone for Comparer { fn clone(&self) -> Self { - Self { - subscribers: self.subscribers.clone(), - } + *self } }