From cfd68bf7d9ac94851928f9772536a8897c6e90d0 Mon Sep 17 00:00:00 2001 From: Evan Almloff Date: Mon, 7 Aug 2023 16:56:49 -0700 Subject: [PATCH] document signals crate --- packages/hooks/src/computed.rs | 210 ------------------------------- packages/hooks/src/lib.rs | 3 - packages/signals/src/effect.rs | 10 ++ packages/signals/src/impls.rs | 60 ++++++++- packages/signals/src/rt.rs | 6 + packages/signals/src/selector.rs | 20 +++ packages/signals/src/signal.rs | 31 +++++ 7 files changed, 123 insertions(+), 217 deletions(-) delete mode 100644 packages/hooks/src/computed.rs diff --git a/packages/hooks/src/computed.rs b/packages/hooks/src/computed.rs deleted file mode 100644 index 248eb4f83..000000000 --- a/packages/hooks/src/computed.rs +++ /dev/null @@ -1,210 +0,0 @@ -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 Selector state -/// -/// 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 { -/// count: count.clone(), -/// } -/// } -/// } -/// -/// #[inline_props] -/// fn Child(cx: Scope, count: Tracked) -> Element { -/// let less_than_five = use_selector(cx, count, |count| *count < 5); -/// -/// 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 Selector state -/// -/// 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: SubscribedCallbacks, -} - -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 Selector state from this tracked state - pub fn compute( - &self, - mut compute: impl FnMut(&I) -> O + 'static, - ) -> 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(); - - Selector { - 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().iter().copied() { - (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: SubscribedCallbacks, -} - -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); - } - } -} - -type SubscribedCallbacks = std::rc::Rc>>>; - -pub(crate) struct Tracker { - subscribers: SubscribedCallbacks, - id: usize, -} - -impl Drop for Tracker { - fn drop(&mut self) { - let _ = self.subscribers.borrow_mut().remove(self.id); - } -} - -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) -} - -/// 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 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(); - 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 7e5798167..65f390eda 100644 --- a/packages/hooks/src/lib.rs +++ b/packages/hooks/src/lib.rs @@ -52,9 +52,6 @@ macro_rules! to_owned { }; } -mod computed; -pub use computed::*; - mod use_on_unmount; pub use use_on_unmount::*; diff --git a/packages/signals/src/effect.rs b/packages/signals/src/effect.rs index 06b02ffd4..be57f5cfd 100644 --- a/packages/signals/src/effect.rs +++ b/packages/signals/src/effect.rs @@ -20,6 +20,13 @@ pub(crate) fn get_effect_stack() -> EffectStack { } } +/// 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(cx: &ScopeState, callback: impl FnMut() + 'static) { + cx.use_hook(|| Effect::new(callback)); +} + +/// 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 { pub(crate) callback: CopyValue>, @@ -36,6 +43,9 @@ impl Effect { get_effect_stack().effects.read().last().copied() } + /// 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 new(callback: impl FnMut() + 'static) -> Self { let myself = Self { callback: CopyValue::new(Box::new(callback)), diff --git a/packages/signals/src/impls.rs b/packages/signals/src/impls.rs index 0328d4063..4e81a5a27 100644 --- a/packages/signals/src/impls.rs +++ b/packages/signals/src/impls.rs @@ -43,6 +43,7 @@ macro_rules! read_impls { } impl $ty> { + /// Unwraps the inner value and clones it. pub fn unwrap(&self) -> T where T: Clone, @@ -50,6 +51,7 @@ macro_rules! read_impls { self.with(|v| v.clone()).unwrap() } + /// Attemps to read the inner value of the Option. pub fn as_ref(&self) -> Option> { Ref::filter_map(self.read(), |v| v.as_ref()).ok() } @@ -59,6 +61,14 @@ macro_rules! read_impls { macro_rules! write_impls { ($ty:ident) => { + impl + Copy + 'static> std::ops::Add for $ty { + type Output = T; + + fn add(self, rhs: T) -> Self::Output { + self.with(|v| *v + rhs) + } + } + impl + Copy + 'static> std::ops::AddAssign for $ty { fn add_assign(&mut self, rhs: T) { self.with_mut(|v| *v = *v + rhs) @@ -71,73 +81,111 @@ macro_rules! write_impls { } } + impl + Copy + 'static> std::ops::Sub for $ty { + type Output = T; + + fn sub(self, rhs: T) -> Self::Output { + self.with(|v| *v - rhs) + } + } + impl + Copy + 'static> std::ops::MulAssign for $ty { fn mul_assign(&mut self, rhs: T) { self.with_mut(|v| *v = *v * rhs) } } + impl + Copy + 'static> std::ops::Mul for $ty { + type Output = T; + + fn mul(self, rhs: T) -> Self::Output { + self.with(|v| *v * rhs) + } + } + impl + Copy + 'static> std::ops::DivAssign for $ty { fn div_assign(&mut self, rhs: T) { self.with_mut(|v| *v = *v / rhs) } } + impl + Copy + 'static> std::ops::Div for $ty { + type Output = T; + + fn div(self, rhs: T) -> Self::Output { + self.with(|v| *v / rhs) + } + } + impl $ty> { + /// Pushes a new value to the end of the vector. pub fn push(&self, value: T) { self.with_mut(|v| v.push(value)) } + /// Pops the last value from the vector. pub fn pop(&self) -> Option { self.with_mut(|v| v.pop()) } + /// Inserts a new value at the given index. pub fn insert(&self, index: usize, value: T) { self.with_mut(|v| v.insert(index, value)) } + /// Removes the value at the given index. pub fn remove(&self, index: usize) -> T { self.with_mut(|v| v.remove(index)) } + /// Clears the vector, removing all values. pub fn clear(&self) { self.with_mut(|v| v.clear()) } + /// Extends the vector with the given iterator. pub fn extend(&self, iter: impl IntoIterator) { self.with_mut(|v| v.extend(iter)) } + /// Truncates the vector to the given length. pub fn truncate(&self, len: usize) { self.with_mut(|v| v.truncate(len)) } + /// Swaps two values in the vector. pub fn swap_remove(&self, index: usize) -> T { self.with_mut(|v| v.swap_remove(index)) } + /// Retains only the values that match the given predicate. pub fn retain(&self, f: impl FnMut(&T) -> bool) { self.with_mut(|v| v.retain(f)) } + /// Splits the vector into two at the given index. pub fn split_off(&self, at: usize) -> Vec { self.with_mut(|v| v.split_off(at)) } } impl $ty> { + /// Takes the value out of the Option. pub fn take(&self) -> Option { self.with_mut(|v| v.take()) } + /// Replace the value in the Option. pub fn replace(&self, value: T) -> Option { self.with_mut(|v| v.replace(value)) } + /// Gets the value out of the Option, or inserts the given value if the Option is empty. pub fn get_or_insert(&self, default: T) -> Ref<'_, T> { self.get_or_insert_with(|| default) } + /// Gets the value out of the Option, or inserts the value returned by the given function if the Option is empty. pub fn get_or_insert_with(&self, default: impl FnOnce() -> T) -> Ref<'_, T> { let borrow = self.read(); if borrow.is_none() { @@ -158,6 +206,7 @@ read_impls!(Signal); write_impls!(Signal); read_impls!(ReadOnlySignal); +/// An iterator over the values of a `CopyValue>`. pub struct CopyValueIterator { index: usize, value: CopyValue>, @@ -198,12 +247,13 @@ impl CopyValue> { } } -pub struct CopySignalIterator { +/// An iterator over items in a `Signal>`. +pub struct SignalIterator { index: usize, value: Signal>, } -impl Iterator for CopySignalIterator { +impl Iterator for SignalIterator { type Item = T; fn next(&mut self) -> Option { @@ -214,12 +264,12 @@ impl Iterator for CopySignalIterator { } impl IntoIterator for Signal> { - type IntoIter = CopySignalIterator; + type IntoIter = SignalIterator; type Item = T; fn into_iter(self) -> Self::IntoIter { - CopySignalIterator { + SignalIterator { index: 0, value: self, } @@ -227,12 +277,14 @@ impl IntoIterator for Signal> { } impl Signal> { + /// Returns a reference to an element or `None` if out of bounds. pub fn get_mut(&self, index: usize) -> Option>> { Write::filter_map(self.write(), |v| v.get_mut(index)) } } impl Signal> { + /// Returns a reference to an element or `None` if out of bounds. pub fn as_mut(&self) -> Option>> { Write::filter_map(self.write(), |v| v.as_mut()) } diff --git a/packages/signals/src/rt.rs b/packages/signals/src/rt.rs index f96ff6ab3..dc1c4a66f 100644 --- a/packages/signals/src/rt.rs +++ b/packages/signals/src/rt.rs @@ -40,12 +40,18 @@ fn owner_in_scope(scope: ScopeId) -> Rc { } } +/// CopyValue is a wrapper around a value to make the value mutable and Copy. +/// +/// It is internally backed by [`generational_box::GenerationalBox`]. pub struct CopyValue { pub(crate) value: GenerationalBox, origin_scope: ScopeId, } impl CopyValue { + /// Create a new CopyValue. The value will be stored in the current component. + /// + /// Once the component this value is created in is dropped, the value will be dropped. pub fn new(value: T) -> Self { let owner = current_owner(); diff --git a/packages/signals/src/selector.rs b/packages/signals/src/selector.rs index dd02031ad..79a8f679b 100644 --- a/packages/signals/src/selector.rs +++ b/packages/signals/src/selector.rs @@ -2,6 +2,23 @@ use dioxus_core::prelude::*; 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. +/// +/// 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 count = use_signal(cx, || 0); +/// let double = use_selector(cx, move || count * 2); +/// count += 1; +/// assert_eq!(double.value(), count * 2); +/// +/// render! { "{double}" } +/// } +/// ``` pub fn use_selector( cx: &ScopeState, f: impl FnMut() -> R + 'static, @@ -9,6 +26,9 @@ pub fn use_selector( *cx.use_hook(|| selector(f)) } +/// 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. pub fn selector(mut f: impl FnMut() -> R + 'static) -> ReadOnlySignal { let state = Signal:: { inner: CopyValue::invalid(), diff --git a/packages/signals/src/signal.rs b/packages/signals/src/signal.rs index 0c45da75a..698ac8e12 100644 --- a/packages/signals/src/signal.rs +++ b/packages/signals/src/signal.rs @@ -12,6 +12,37 @@ use dioxus_core::{ use crate::{CopyValue, Effect}; +/// Creates a new Signal. Signals are a Copy state management solution with automatic dependency tracking. +/// +/// ```rust +/// use dioxus::prelude::*; +/// use dioxus_signals::*; +/// +/// fn App(cx: Scope) -> Element { +/// let mut count = use_signal(cx, || 0); +/// +/// // Because signals have automatic dependency tracking, if you never read them in a component, that component will not be re-rended when the signal is updated. +/// // The app component will never be rerendered in this example. +/// render! { Child { state: count } } +/// } +/// +/// #[inline_props] +/// fn Child(cx: Scope, state: Signal) -> Element { +/// let state = *state; +/// +/// use_future!(cx, |()| async move { +/// // Because the signal is a Copy type, we can use it in an async block without cloning it. +/// *state.write() += 1; +/// }); +/// +/// render! { +/// button { +/// onclick: move |_| *state.write() += 1, +/// "{state}" +/// } +/// } +/// } +/// ``` pub fn use_signal(cx: &ScopeState, f: impl FnOnce() -> T) -> Signal { *cx.use_hook(|| Signal::new(f())) }