From 33ee0636d60e3b98e64e6d8548f5de975a5f581d Mon Sep 17 00:00:00 2001 From: Evan Almloff Date: Thu, 20 Jul 2023 11:07:55 -0700 Subject: [PATCH 1/2] add computed hook --- packages/hooks/Cargo.toml | 1 + packages/hooks/src/computed.rs | 198 +++++++++++++++++++++++++++++++++ packages/hooks/src/lib.rs | 3 + 3 files changed, 202 insertions(+) create mode 100644 packages/hooks/src/computed.rs diff --git a/packages/hooks/Cargo.toml b/packages/hooks/Cargo.toml index e4e10809c..fb4efdf63 100644 --- a/packages/hooks/Cargo.toml +++ b/packages/hooks/Cargo.toml @@ -19,6 +19,7 @@ futures-channel = { workspace = true } log = { workspace = true } thiserror = { workspace = true } debug-cell = { git = "https://github.com/Niedzwiedzw/debug-cell", rev = "3352a1f8aff19f56f5e3b2018200a3338fd43d2e" } # waiting for the merge / official DioxusLabs fork +slab.workspace = true [dev-dependencies] futures-util = { workspace = true, default-features = false } diff --git a/packages/hooks/src/computed.rs b/packages/hooks/src/computed.rs new file mode 100644 index 000000000..16e9981e3 --- /dev/null +++ b/packages/hooks/src/computed.rs @@ -0,0 +1,198 @@ +use dioxus_core::{ScopeId, ScopeState}; +use slab::Slab; +use std::{ + cell::{RefCell, RefMut}, + collections::HashSet, + ops::{Deref, DerefMut}, + rc::Rc, +}; + +/// Create a new tracked state. +/// Tracked state is state that can drive computed state +/// +/// It state will efficiently update any Computed state that is reading from it, but it is not readable on it's own. +/// +/// ```rust +/// use dioxus::prelude::*; +/// +/// fn Parent(cx: Scope) -> Element { +/// let count = use_tracked_state(cx, || 0); +/// render! { +/// Child { +/// less_than_five: count.compute(|count| *count < 5), +/// } +/// } +/// } +/// +/// #[inline_props] +/// fn Child(cx: Scope, less_than_five: Computed) -> Element { +/// let less_than_five = less_than_five.use_state(cx); +/// +/// render! { +/// "{less_than_five}" +/// } +/// } +/// ``` +pub fn use_tracked_state(cx: &ScopeState, init: impl FnOnce() -> T) -> &Tracked { + cx.use_hook(|| { + let init = init(); + Tracked::new(cx, init) + }) +} + +/// Tracked state is state that can drive computed state +/// +/// Tracked state will efficiently update any Computed state that is reading from it, but it is not readable on it's own. +#[derive(Clone)] +pub struct Tracked { + state: Rc>, + update_any: std::sync::Arc, + subscribers: std::rc::Rc>>>, +} + +impl PartialEq for Tracked { + fn eq(&self, other: &Self) -> bool { + self.state == other.state + } +} + +impl Tracked { + /// Create a new tracked state + pub fn new(cx: &ScopeState, state: I) -> Self { + let subscribers = std::rc::Rc::new(std::cell::RefCell::new(Slab::new())); + Self { + state: Rc::new(RefCell::new(state)), + subscribers, + update_any: cx.schedule_update_any(), + } + } + + /// Create a new computed state from this tracked state + pub fn compute( + &self, + mut compute: impl FnMut(&I) -> O + 'static, + ) -> Computed { + let subscribers = Rc::new(RefCell::new(HashSet::new())); + let state = Rc::new(RefCell::new(compute(&self.state.borrow()))); + let update_any = self.update_any.clone(); + + Computed { + value: state.clone(), + subscribers: subscribers.clone(), + _tracker: Rc::new(self.track(move |input_state| { + let new = compute(input_state); + let different = { + let state = state.borrow(); + *state != new + }; + if different { + let mut state = state.borrow_mut(); + *state = new; + for id in subscribers.borrow_mut().drain() { + (update_any)(id); + } + } + })), + } + } + + pub(crate) fn track(&self, update: impl FnMut(&I) + 'static) -> Tracker { + let mut subscribers = self.subscribers.borrow_mut(); + let id = subscribers.insert(Box::new(update)); + Tracker { + subscribers: self.subscribers.clone(), + id, + } + } + + /// Write to the tracked state + pub fn write(&self) -> TrackedMut<'_, I> { + TrackedMut { + state: self.state.borrow_mut(), + subscribers: self.subscribers.clone(), + } + } +} + +/// A mutable reference to tracked state +pub struct TrackedMut<'a, I> { + state: RefMut<'a, I>, + subscribers: std::rc::Rc>>>, +} + +impl<'a, I> Deref for TrackedMut<'a, I> { + type Target = I; + + fn deref(&self) -> &Self::Target { + &self.state + } +} + +impl<'a, I> DerefMut for TrackedMut<'a, I> { + fn deref_mut(&mut self) -> &mut Self::Target { + &mut self.state + } +} + +impl<'a, I> Drop for TrackedMut<'a, I> { + fn drop(&mut self) { + let state = self.state.deref(); + for (_, sub) in &mut *self.subscribers.borrow_mut() { + sub(state); + } + } +} + +pub(crate) struct Tracker { + subscribers: std::rc::Rc>>>, + id: usize, +} + +impl Drop for Tracker { + fn drop(&mut self) { + let _ = self.subscribers.borrow_mut().remove(self.id); + } +} + +/// Computed state is state that is derived from tracked state +/// +/// Whenever the tracked state changes, the computed state will be updated and any components reading from it will be rerun +#[derive(Clone)] +pub struct Computed { + _tracker: Rc>, + value: Rc>, + subscribers: std::rc::Rc>>, +} + +impl PartialEq for Computed { + fn eq(&self, other: &Self) -> bool { + std::rc::Rc::ptr_eq(&self.value, &other.value) + } +} + +impl Computed { + /// Read the computed state and subscribe to updates + pub fn use_state(&self, cx: &ScopeState) -> T { + cx.use_hook(|| { + let id = cx.scope_id(); + self.subscribers.borrow_mut().insert(id); + + ComputedRead { + scope: cx.scope_id(), + subscribers: self.subscribers.clone(), + } + }); + self.value.borrow().clone() + } +} + +struct ComputedRead { + scope: ScopeId, + subscribers: std::rc::Rc>>, +} + +impl Drop for ComputedRead { + fn drop(&mut self) { + self.subscribers.borrow_mut().remove(&self.scope); + } +} diff --git a/packages/hooks/src/lib.rs b/packages/hooks/src/lib.rs index 65f390eda..7e5798167 100644 --- a/packages/hooks/src/lib.rs +++ b/packages/hooks/src/lib.rs @@ -52,6 +52,9 @@ macro_rules! to_owned { }; } +mod computed; +pub use computed::*; + mod use_on_unmount; pub use use_on_unmount::*; From d7f7116ce3b2348f16986defc640a1c467e3d420 Mon Sep 17 00:00:00 2001 From: Evan Almloff Date: Fri, 28 Jul 2023 14:17:52 -0700 Subject: [PATCH 2/2] clean up the API a bit --- packages/hooks/src/computed.rs | 62 ++++++++++++++++++++-------------- 1 file changed, 37 insertions(+), 25 deletions(-) diff --git a/packages/hooks/src/computed.rs b/packages/hooks/src/computed.rs index 16e9981e3..248eb4f83 100644 --- a/packages/hooks/src/computed.rs +++ b/packages/hooks/src/computed.rs @@ -8,25 +8,26 @@ use std::{ }; /// Create a new tracked state. -/// Tracked state is state that can drive computed state +/// Tracked state is state that can drive Selector state /// -/// It state will efficiently update any Computed state that is reading from it, but it is not readable on it's own. +/// It state will efficiently update any Selector state that is reading from it, but it is not readable on it's own. /// /// ```rust /// use dioxus::prelude::*; /// /// fn Parent(cx: Scope) -> Element { /// let count = use_tracked_state(cx, || 0); +/// /// render! { /// Child { -/// less_than_five: count.compute(|count| *count < 5), +/// count: count.clone(), /// } /// } /// } /// /// #[inline_props] -/// fn Child(cx: Scope, less_than_five: Computed) -> Element { -/// let less_than_five = less_than_five.use_state(cx); +/// fn Child(cx: Scope, count: Tracked) -> Element { +/// let less_than_five = use_selector(cx, count, |count| *count < 5); /// /// render! { /// "{less_than_five}" @@ -40,14 +41,14 @@ pub fn use_tracked_state(cx: &ScopeState, init: impl FnOnce() -> T) }) } -/// Tracked state is state that can drive computed state +/// Tracked state is state that can drive Selector state /// -/// Tracked state will efficiently update any Computed state that is reading from it, but it is not readable on it's own. +/// Tracked state will efficiently update any Selector state that is reading from it, but it is not readable on it's own. #[derive(Clone)] pub struct Tracked { state: Rc>, update_any: std::sync::Arc, - subscribers: std::rc::Rc>>>, + subscribers: SubscribedCallbacks, } impl PartialEq for Tracked { @@ -67,16 +68,16 @@ impl Tracked { } } - /// Create a new computed state from this tracked state + /// Create a new Selector state from this tracked state pub fn compute( &self, mut compute: impl FnMut(&I) -> O + 'static, - ) -> Computed { + ) -> Selector { let subscribers = Rc::new(RefCell::new(HashSet::new())); let state = Rc::new(RefCell::new(compute(&self.state.borrow()))); let update_any = self.update_any.clone(); - Computed { + Selector { value: state.clone(), subscribers: subscribers.clone(), _tracker: Rc::new(self.track(move |input_state| { @@ -88,7 +89,7 @@ impl Tracked { if different { let mut state = state.borrow_mut(); *state = new; - for id in subscribers.borrow_mut().drain() { + for id in subscribers.borrow().iter().copied() { (update_any)(id); } } @@ -117,7 +118,7 @@ impl Tracked { /// A mutable reference to tracked state pub struct TrackedMut<'a, I> { state: RefMut<'a, I>, - subscribers: std::rc::Rc>>>, + subscribers: SubscribedCallbacks, } impl<'a, I> Deref for TrackedMut<'a, I> { @@ -143,8 +144,10 @@ impl<'a, I> Drop for TrackedMut<'a, I> { } } +type SubscribedCallbacks = std::rc::Rc>>>; + pub(crate) struct Tracker { - subscribers: std::rc::Rc>>>, + subscribers: SubscribedCallbacks, id: usize, } @@ -154,24 +157,33 @@ impl Drop for Tracker { } } -/// Computed state is state that is derived from tracked state -/// -/// Whenever the tracked state changes, the computed state will be updated and any components reading from it will be rerun -#[derive(Clone)] -pub struct Computed { - _tracker: Rc>, - value: Rc>, - subscribers: std::rc::Rc>>, +pub fn use_selector( + cx: &ScopeState, + tracked: &Tracked, + init: impl FnMut(&I) -> O + 'static, +) -> O { + let selector = cx.use_hook(|| tracked.compute(init)); + selector.use_state(cx) } -impl PartialEq for Computed { +/// Selector state is state that is derived from tracked state +/// +/// Whenever the tracked state changes, the Selector state will be updated and any components reading from it will be rerun +#[derive(Clone)] +pub struct Selector { + _tracker: Rc>, + value: Rc>, + subscribers: Rc>>, +} + +impl PartialEq for Selector { fn eq(&self, other: &Self) -> bool { std::rc::Rc::ptr_eq(&self.value, &other.value) } } -impl Computed { - /// Read the computed state and subscribe to updates +impl Selector { + /// Read the Selector state and subscribe to updates pub fn use_state(&self, cx: &ScopeState) -> T { cx.use_hook(|| { let id = cx.scope_id();