From 33ee0636d60e3b98e64e6d8548f5de975a5f581d Mon Sep 17 00:00:00 2001 From: Evan Almloff Date: Thu, 20 Jul 2023 11:07:55 -0700 Subject: [PATCH] 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::*;