From d815c7cc8980c1b8592c131314af3b63867347c0 Mon Sep 17 00:00:00 2001 From: Evan Almloff Date: Tue, 31 Oct 2023 18:30:06 -0500 Subject: [PATCH] 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 {