From c17fb79be21d89433abb1bb37507e7fbf1b466fa Mon Sep 17 00:00:00 2001 From: Kestrer Date: Mon, 8 Mar 2021 09:20:37 +0000 Subject: [PATCH 1/4] Replace functions with Signal type --- examples/components/src/main.rs | 11 +- examples/counter/src/main.rs | 15 +- examples/hello/src/main.rs | 21 +- maple-core/src/lib.rs | 5 +- maple-core/src/reactive.rs | 877 +++++++++++++++++--------------- 5 files changed, 491 insertions(+), 438 deletions(-) diff --git a/examples/components/src/main.rs b/examples/components/src/main.rs index b635807b0..c058b889e 100644 --- a/examples/components/src/main.rs +++ b/examples/components/src/main.rs @@ -8,7 +8,7 @@ pub fn MyComponent(num: StateHandle) -> TemplateResult { # "My component" p { # "Value: " - # num() + # num.get() } } } @@ -18,13 +18,12 @@ fn main() { console_error_panic_hook::set_once(); console_log::init_with_level(log::Level::Debug).unwrap(); - let (state, set_state) = create_signal(1); + let state = Signal::new(1); let increment = { let state = state.clone(); - let set_state = set_state.clone(); move |_| { - set_state(*state() + 1); + state.set(*state.get() + 1); } }; @@ -34,8 +33,8 @@ fn main() { # "Component demo" } - MyComponent(state.clone()) - MyComponent(state.clone()) + MyComponent((*state).clone()) + MyComponent((*state).clone()) button(on:click=increment) { # "Increment" diff --git a/examples/counter/src/main.rs b/examples/counter/src/main.rs index 985588dde..fe069ef4d 100644 --- a/examples/counter/src/main.rs +++ b/examples/counter/src/main.rs @@ -4,26 +4,23 @@ fn main() { console_error_panic_hook::set_once(); console_log::init_with_level(log::Level::Debug).unwrap(); - let (counter, set_counter) = create_signal(0); + let counter = Signal::new(0); create_effect({ let counter = counter.clone(); move || { - log::info!("Counter value: {}", *counter()); + log::info!("Counter value: {}", *counter.get()); } }); let increment = { let counter = counter.clone(); - let set_counter = set_counter.clone(); - - move |_| set_counter(*counter() + 1) + move |_| counter.set(*counter.get() + 1) }; let reset = { - let set_counter = set_counter.clone(); - - move |_| set_counter(0) + let counter = counter.clone(); + move |_| counter.set(0) }; let root = template! { @@ -31,7 +28,7 @@ fn main() { # "Counter demo" p(class="value") { # "Value: " - # counter() + # counter.get() } button(class="increment", on:click=increment) { # "Increment" diff --git a/examples/hello/src/main.rs b/examples/hello/src/main.rs index e72da0743..a277e8ef4 100644 --- a/examples/hello/src/main.rs +++ b/examples/hello/src/main.rs @@ -8,18 +8,21 @@ fn main() { console_error_panic_hook::set_once(); console_log::init_with_level(log::Level::Debug).unwrap(); - let (name, set_name) = create_signal(String::new()); - - let displayed_name = create_memo(move || { - if *name() == "" { - "World".to_string() - } else { - name().as_ref().clone() + let name = Signal::new(String::new()); + + let displayed_name = create_memo({ + let name = name.clone(); + move || { + if name.get().is_empty() { + "World".to_string() + } else { + name.get().as_ref().clone() + } } }); let handle_change = move |event: Event| { - set_name( + name.set( event .target() .unwrap() @@ -33,7 +36,7 @@ fn main() { div { h1 { # "Hello " - # displayed_name() + # displayed_name.get() # "!" } diff --git a/maple-core/src/lib.rs b/maple-core/src/lib.rs index bd94aa38c..25394c3c7 100644 --- a/maple-core/src/lib.rs +++ b/maple-core/src/lib.rs @@ -44,10 +44,7 @@ impl TemplateResult { /// The maple prelude. pub mod prelude { - pub use crate::reactive::{ - create_effect, create_memo, create_selector, create_signal, untracked, SetStateHandle, - StateHandle, - }; + pub use crate::reactive::{create_effect, create_memo, create_selector, Signal, StateHandle}; pub use crate::{render, TemplateResult}; pub use maple_core_macro::template; diff --git a/maple-core/src/reactive.rs b/maple-core/src/reactive.rs index 411f3aef2..df22e83ed 100644 --- a/maple-core/src/reactive.rs +++ b/maple-core/src/reactive.rs @@ -1,410 +1,467 @@ -//! Reactive primitives. - -use std::cell::RefCell; -use std::rc::Rc; - -/// Returned by functions that provide a handle to access state. -pub type StateHandle = Rc Rc>; - -/// Returned by functions that provide a closure to modify state. -pub type SetStateHandle = Rc; - -struct Signal { - inner: Rc, - observers: Vec>, -} - -impl Signal { - fn new(value: T) -> Self { - Self { - inner: Rc::new(value), - observers: Vec::new(), - } - } - - fn observe(&mut self, handler: Rc) { - // make sure handler is not already in self.observers - if self - .observers - .iter() - .find(|observer| { - observer.as_ref() as *const Computation == handler.as_ref() as *const Computation - /* do reference equality */ - }) - .is_none() - { - self.observers.push(handler); - } - } - - fn update(&mut self, new_value: T) { - self.inner = Rc::new(new_value); - } - - fn trigger_observers(&self) { - for observer in &self.observers { - observer.0(); - } - } -} - -/// A derived computation from a signal. -struct Computation(Box); - -thread_local! { - static HANDLER: RefCell>> = RefCell::new(None); - - /// To add the dependencies, iterate through functions and execute them. - static DEPENDENCIES: RefCell>>> = RefCell::new(None); -} - -/// Creates a new signal. -/// The function will return a pair of getter/setters to modify the signal and update corresponding dependencies. -/// -/// # Example -/// ```rust -/// use maple_core::prelude::*; -/// -/// let (state, set_state) = create_signal(0); -/// assert_eq!(*state(), 0); -/// -/// set_state(1); -/// assert_eq!(*state(), 1); -/// ``` -pub fn create_signal(value: T) -> (StateHandle, SetStateHandle) { - let signal = Rc::new(RefCell::new(Signal::new(value))); - - let getter = { - let signal = signal.clone(); - move || { - // if inside an effect, add this signal to dependency list - DEPENDENCIES.with(|dependencies| { - if dependencies.borrow().is_some() { - let signal = signal.clone(); - let handler = - HANDLER.with(|handler| handler.borrow().as_ref().unwrap().clone()); - - dependencies - .borrow_mut() - .as_mut() - .unwrap() - .push(Box::new(move || { - signal.borrow_mut().observe(handler.clone()) - })); - } - }); - - signal.borrow().inner.clone() - } - }; - - let setter = { - let signal = signal.clone(); - move |new_value| { - match signal.try_borrow_mut() { - Ok(mut signal) => signal.update(new_value), - // If the signal is already borrowed, that means it is borrowed in the getter, thus creating a cyclic dependency. - Err(_err) => panic!("cannot create cyclic dependency"), - }; - signal.borrow().trigger_observers(); - } - }; - - (Rc::new(getter), Rc::new(setter)) -} - -/// Creates an effect on signals used inside the effect closure. -pub fn create_effect(effect: F) -where - F: Fn() + 'static, -{ - DEPENDENCIES.with(|dependencies| { - if dependencies.borrow().is_some() { - unimplemented!("nested dependencies are not supported") - } - - let effect = Rc::new(Computation(Box::new(effect))); - - *dependencies.borrow_mut() = Some(Vec::new()); - HANDLER.with(|handler| *handler.borrow_mut() = Some(effect.clone())); - - // run effect for the first time to attach all the dependencies - effect.0(); - - // attach dependencies - for dependency in dependencies.borrow().as_ref().unwrap() { - dependency(); - } - - // Reset dependencies for next effect hook - *dependencies.borrow_mut() = None; - }) -} - -/// Prevents tracking dependencies inside the closure. If called outside a reactive context, does nothing. -/// -/// # Example -/// ```rust -/// use maple_core::prelude::*; -/// -/// let (state, set_state) = create_signal(1); -/// -/// let double = create_memo(move || untracked(|| *state()) * 2); -/// -/// assert_eq!(*double(), 2); -/// -/// set_state(2); -/// assert_eq!(*double(), 2); // double value should still be old value because state() was inside untracked -/// ``` -pub fn untracked(f: F) -> Out -where - F: Fn() -> Out, -{ - let tmp = DEPENDENCIES.with(|dependencies| dependencies.take()); - let out = f(); - DEPENDENCIES.with(|dependencies| *dependencies.borrow_mut() = tmp); - - out -} - -/// Creates a memoized value from some signals. Also know as "derived stores". -pub fn create_memo(derived: F) -> StateHandle -where - F: Fn() -> Out + 'static, - Out: Clone + 'static, -{ - let derived = Rc::new(derived); - let (memo, set_memo) = create_signal(None); - - create_effect({ - let derived = derived.clone(); - move || { - set_memo(Some(derived())); - } - }); - - // return memoized result - let memo_result = move || Rc::new(Option::as_ref(&memo()).unwrap().clone()); - Rc::new(memo_result) -} - -/// Creates a memoized value from some signals. Also know as "derived stores". -/// Unlike [`create_memo`], this function will not notify dependents of a change if the output is the same. -/// That is why the output of the function must implement `PartialEq`. -pub fn create_selector(derived: F) -> StateHandle -where - F: Fn() -> Out + 'static, - Out: Clone + PartialEq + std::fmt::Debug + 'static, -{ - let derived = Rc::new(derived); - let (memo, set_memo) = create_signal(None); - - create_effect({ - let derived = derived.clone(); - let memo = memo.clone(); - move || { - let new_value = Some(derived()); - if *untracked(|| memo()) != new_value { - set_memo(new_value); - } - } - }); - - // return memoized result - let memo_result = move || Rc::new(Option::as_ref(&memo()).unwrap().clone()); - Rc::new(memo_result) -} - -#[cfg(test)] -mod tests { - use super::*; - - #[test] - fn signals() { - let (state, set_state) = create_signal(0); - assert_eq!(*state(), 0); - - set_state(1); - assert_eq!(*state(), 1); - } - - #[test] - fn signal_composition() { - let (state, set_state) = create_signal(0); - - let double = || *state() * 2; - - assert_eq!(double(), 0); - - set_state(1); - assert_eq!(double(), 2); - } - - #[test] - fn effects() { - let (state, set_state) = create_signal(0); - - let (double, set_double) = create_signal(-1); - - create_effect({ - let set_double = set_double.clone(); - move || { - set_double(*state() * 2); - } - }); - assert_eq!(*double(), 0); // calling create_effect should call the effect at least once - - set_state(1); - assert_eq!(*double(), 2); - set_state(2); - assert_eq!(*double(), 4); - } - - #[test] - #[should_panic(expected = "cannot create cyclic dependency")] - fn cyclic_effects_fail() { - let (state, set_state) = create_signal(0); - - create_effect({ - let state = state.clone(); - let set_state = set_state.clone(); - move || { - set_state(*state() + 1); - } - }); - - set_state(1); - } - - #[test] - #[should_panic(expected = "cannot create cyclic dependency")] - fn cyclic_effects_fail_2() { - let (state, set_state) = create_signal(0); - - create_effect({ - let state = state.clone(); - let set_state = set_state.clone(); - move || { - let value = *state(); - set_state(value + 1); - } - }); - - set_state(1); - } - - #[test] - fn effect_should_subscribe_once() { - let (state, set_state) = create_signal(0); - - let (counter, set_counter) = create_signal(0); - create_effect({ - let counter = counter.clone(); - move || { - set_counter(untracked(|| *counter()) + 1); - - // call state() twice but should subscribe once - state(); - state(); - } - }); - - assert_eq!(*counter(), 1); - - set_state(1); - assert_eq!(*counter(), 2); - } - - #[test] - fn memo() { - let (state, set_state) = create_signal(0); - - let double = create_memo(move || *state() * 2); - assert_eq!(*double(), 0); - - set_state(1); - assert_eq!(*double(), 2); - - set_state(2); - assert_eq!(*double(), 4); - } - - #[test] - /// Make sure value is memoized rather than executed on demand. - fn memo_only_run_once() { - let (state, set_state) = create_signal(0); - - let (counter, set_counter) = create_signal(0); - let double = create_memo({ - let counter = counter.clone(); - move || { - set_counter(untracked(|| *counter()) + 1); - - *state() * 2 - } - }); - assert_eq!(*counter(), 1); // once for calculating initial derived state - - set_state(2); - assert_eq!(*counter(), 2); - assert_eq!(*double(), 4); - assert_eq!(*counter(), 2); // should still be 2 after access - } - - #[test] - fn dependency_on_memo() { - let (state, set_state) = create_signal(0); - - let double = create_memo(move || *state() * 2); - - let quadruple = create_memo(move || *double() * 2); - - assert_eq!(*quadruple(), 0); - - set_state(1); - assert_eq!(*quadruple(), 4); - } - - #[test] - fn untracked_memo() { - let (state, set_state) = create_signal(1); - - let double = create_memo(move || untracked(|| *state()) * 2); - - assert_eq!(*double(), 2); - - set_state(2); - assert_eq!(*double(), 2); // double value should still be true because state() was inside untracked - } - - #[test] - fn selector() { - let (state, set_state) = create_signal(0); - - let double = create_selector({ - let state = state.clone(); - move || *state() * 2 - }); - - let (counter, set_counter) = create_signal(0); - create_effect({ - let counter = counter.clone(); - let double = double.clone(); - move || { - set_counter(untracked(|| *counter()) + 1); - - double(); - } - }); - assert_eq!(*double(), 0); - assert_eq!(*counter(), 1); - - set_state(0); - assert_eq!(*double(), 0); - assert_eq!(*counter(), 1); // calling set_state should not trigger the effect - - set_state(2); - assert_eq!(*double(), 4); - assert_eq!(*counter(), 2); - } -} +//! Reactive primitives. + +use std::cell::Cell; +use std::cell::RefCell; +use std::ops::Deref; +use std::ptr::NonNull; +use std::rc::Rc; + +/// Returned by functions that provide a handle to access state. +pub struct StateHandle(Rc>>); + +impl StateHandle { + /// Get the current value of the state. + pub fn get(&self) -> Rc { + // if inside an effect, add this signal to dependency list + DEPENDENCIES.with(|dependencies| { + if dependencies.borrow().is_some() { + let signal = self.0.clone(); + let handler = HANDLER.with(|handler| handler.borrow().as_ref().unwrap().clone()); + + dependencies + .borrow_mut() + .as_mut() + .unwrap() + .push(Box::new(move || { + signal.borrow_mut().observe(handler.clone()) + })); + } + }); + + self.get_untracked() + } + + /// Get the current value of the state, without tracking this as a dependency if inside a + /// reactive context. + /// + /// # Example + /// + /// ``` + /// use maple_core::prelude::*; + /// + /// let state = Signal::new(1); + /// + /// let double = create_memo({ + /// let state = state.clone(); + /// move || *state.get_untracked() * 2 + /// }); + /// + /// assert_eq!(*double.get(), 2); + /// + /// state.set(2); + /// // double value should still be old value because state was untracked + /// assert_eq!(*double.get(), 2); + /// ``` + pub fn get_untracked(&self) -> Rc { + self.0.borrow().inner.clone() + } +} + +impl Clone for StateHandle { + fn clone(&self) -> Self { + Self(self.0.clone()) + } +} + +/// State that can be set. +pub struct Signal(StateHandle); + +impl Signal { + /// Creates a new signal. + /// + /// # Examples + /// + /// ``` + /// use maple_core::prelude::*; + /// + /// let state = Signal::new(0); + /// assert_eq!(*state.get(), 0); + /// + /// state.set(1); + /// assert_eq!(*state.get(), 1); + /// ``` + pub fn new(value: T) -> Self { + Self(StateHandle(Rc::new(RefCell::new(SignalInner::new(value))))) + } + + /// Set the current value of the state. + /// + /// This will notify and update any effects and memos that depend on this value. + pub fn set(&self, new_value: T) { + match self.0 .0.try_borrow_mut() { + Ok(mut signal) => signal.update(new_value), + // If the signal is already borrowed, that means it is borrowed in the getter, thus creating a cyclic dependency. + Err(_err) => panic!("cannot create cyclic dependency"), + } + self.0 .0.borrow().trigger_observers(); + } +} + +impl Deref for Signal { + type Target = StateHandle; + + fn deref(&self) -> &Self::Target { + &self.0 + } +} + +impl Clone for Signal { + fn clone(&self) -> Self { + Self(self.0.clone()) + } +} + +struct SignalInner { + inner: Rc, + observers: Vec>, +} + +impl SignalInner { + fn new(value: T) -> Self { + Self { + inner: Rc::new(value), + observers: Vec::new(), + } + } + + fn observe(&mut self, handler: Rc) { + // make sure handler is not already in self.observers + if self + .observers + .iter() + .find(|observer| { + observer.as_ref() as *const Computation == handler.as_ref() as *const Computation + /* do reference equality */ + }) + .is_none() + { + self.observers.push(handler); + } + } + + fn update(&mut self, new_value: T) { + self.inner = Rc::new(new_value); + } + + fn trigger_observers(&self) { + for observer in &self.observers { + observer.0(); + } + } +} + +/// A derived computation from a signal. +struct Computation(Box); + +thread_local! { + static HANDLER: RefCell>> = RefCell::new(None); + + /// To add the dependencies, iterate through functions and execute them. + static DEPENDENCIES: RefCell>>> = RefCell::new(None); +} + +/// Creates an effect on signals used inside the effect closure. +pub fn create_effect(effect: F) +where + F: Fn() + 'static, +{ + DEPENDENCIES.with(|dependencies| { + if dependencies.borrow().is_some() { + unimplemented!("nested dependencies are not supported") + } + + let effect = Rc::new(Computation(Box::new(effect))); + + *dependencies.borrow_mut() = Some(Vec::new()); + HANDLER.with(|handler| *handler.borrow_mut() = Some(effect.clone())); + + // run effect for the first time to attach all the dependencies + effect.0(); + + // attach dependencies + for dependency in dependencies.borrow().as_ref().unwrap() { + dependency(); + } + + // Reset dependencies for next effect hook + *dependencies.borrow_mut() = None; + }) +} + +/// Creates a memoized value from some signals. Also know as "derived stores". +pub fn create_memo(derived: F) -> StateHandle +where + F: Fn() -> Out + 'static, + Out: 'static, +{ + create_selector_with(derived, |_, _| false) +} + +/// Creates a memoized value from some signals. Also know as "derived stores". +/// Unlike [`create_memo`], this function will not notify dependents of a change if the output is the same. +/// That is why the output of the function must implement [`PartialEq`]. +/// +/// To specify a custom comparison function, use [`create_selector_with`]. +pub fn create_selector(derived: F) -> StateHandle +where + F: Fn() -> Out + 'static, + Out: PartialEq + 'static, +{ + create_selector_with(derived, PartialEq::eq) +} + +/// Creates a memoized value from some signals. Also know as "derived stores". +/// Unlike [`create_memo`], this function will not notify dependents of a change if the output is the same. +/// +/// It takes a comparison function to compare the old and new value, which returns `true` if they +/// are the same and `false` otherwise. +/// +/// To use the type's [`PartialEq`] implementation instead of a custom function, use +/// [`create_selector`]. +pub fn create_selector_with(derived: F, comparator: C) -> StateHandle +where + F: Fn() -> Out + 'static, + Out: 'static, + C: Fn(&Out, &Out) -> bool + 'static, +{ + enum EffectState { + Uninitialized(NonNull>>), + Initialized(Signal), + Temporary, + } + + let mut handle = None; + let state = Cell::new(EffectState::Uninitialized(NonNull::from(&mut handle))); + + create_effect(move || { + let memo = match state.replace(EffectState::Temporary) { + // We haven't been called yet, so initialize the signal. + EffectState::Uninitialized(mut handle_ptr) => { + let memo = Signal::new(derived()); + + // SAFETY: This is the first time the function is called, and so this pointer is + // still valid. + *unsafe { handle_ptr.as_mut() } = Some((*memo).clone()); + + memo + } + // We have been called; update the signal. + EffectState::Initialized(memo) => { + let new_value = derived(); + if !comparator(&memo.get_untracked(), &new_value) { + memo.set(new_value); + } + memo + } + EffectState::Temporary => unreachable!(), + }; + state.replace(EffectState::Initialized(memo)); + }); + + // By this time, the above closure will have been run once, setting the `handle` value. + handle.unwrap() +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn signals() { + let state = Signal::new(0); + assert_eq!(*state.get(), 0); + + state.set(1); + assert_eq!(*state.get(), 1); + } + + #[test] + fn signal_composition() { + let state = Signal::new(0); + + let double = || *state.get() * 2; + + assert_eq!(double(), 0); + + state.set(1); + assert_eq!(double(), 2); + } + + #[test] + fn effects() { + let state = Signal::new(0); + + let double = Signal::new(-1); + + create_effect({ + let state = state.clone(); + let double = double.clone(); + move || { + double.set(*state.get() * 2); + } + }); + assert_eq!(*double.get(), 0); // calling create_effect should call the effect at least once + + state.set(1); + assert_eq!(*double.get(), 2); + state.set(2); + assert_eq!(*double.get(), 4); + } + + #[test] + #[should_panic(expected = "cannot create cyclic dependency")] + fn cyclic_effects_fail() { + let state = Signal::new(0); + + create_effect({ + let state = state.clone(); + move || { + state.set(*state.get() + 1); + } + }); + + state.set(1); + } + + #[test] + #[should_panic(expected = "cannot create cyclic dependency")] + fn cyclic_effects_fail_2() { + let state = Signal::new(0); + + create_effect({ + let state = state.clone(); + move || { + let value = *state.get(); + state.set(value + 1); + } + }); + + state.set(1); + } + + #[test] + fn effect_should_subscribe_once() { + let state = Signal::new(0); + + let counter = Signal::new(0); + create_effect({ + let state = state.clone(); + let counter = counter.clone(); + move || { + counter.set(*counter.get_untracked() + 1); + + // call state.get() twice but should subscribe once + state.get(); + state.get(); + } + }); + + assert_eq!(*counter.get(), 1); + + state.set(1); + assert_eq!(*counter.get(), 2); + } + + #[test] + fn memo() { + let state = Signal::new(0); + + let double = create_memo({ + let state = state.clone(); + move || *state.get() * 2 + }); + assert_eq!(*double.get(), 0); + + state.set(1); + assert_eq!(*double.get(), 2); + + state.set(2); + assert_eq!(*double.get(), 4); + } + + #[test] + /// Make sure value is memoized rather than executed on demand. + fn memo_only_run_once() { + let state = Signal::new(0); + + let counter = Signal::new(0); + let double = create_memo({ + let state = state.clone(); + let counter = counter.clone(); + move || { + counter.set(*counter.get_untracked() + 1); + + *state.get() * 2 + } + }); + assert_eq!(*counter.get(), 1); // once for calculating initial derived state + + state.set(2); + assert_eq!(*counter.get(), 2); + assert_eq!(*double.get(), 4); + assert_eq!(*counter.get(), 2); // should still be 2 after access + } + + #[test] + fn dependency_on_memo() { + let state = Signal::new(0); + + let double = create_memo({ + let state = state.clone(); + move || *state.get() * 2 + }); + + let quadruple = create_memo(move || *double.get() * 2); + + assert_eq!(*quadruple.get(), 0); + + state.set(1); + assert_eq!(*quadruple.get(), 4); + } + + #[test] + fn untracked_memo() { + let state = Signal::new(1); + + let double = create_memo({ + let state = state.clone(); + move || *state.get_untracked() * 2 + }); + + assert_eq!(*double.get(), 2); + + state.set(2); + assert_eq!(*double.get(), 2); // double value should still be true because state.get() was inside untracked + } + + #[test] + fn selector() { + let state = Signal::new(0); + + let double = create_selector({ + let state = state.clone(); + move || *state.get() * 2 + }); + + let counter = Signal::new(0); + create_effect({ + let counter = counter.clone(); + let double = double.clone(); + move || { + counter.set(*counter.get_untracked() + 1); + + double.get(); + } + }); + assert_eq!(*double.get(), 0); + assert_eq!(*counter.get(), 1); + + state.set(0); + assert_eq!(*double.get(), 0); + assert_eq!(*counter.get(), 1); // calling set_state should not trigger the effect + + state.set(2); + assert_eq!(*double.get(), 4); + assert_eq!(*counter.get(), 2); + } +} From a5080b5b216964e8a9beb0632f1d4d8d79dae262 Mon Sep 17 00:00:00 2001 From: Kestrer Date: Mon, 8 Mar 2021 12:26:05 +0000 Subject: [PATCH 2/4] Use DOS line endings --- maple-core/src/reactive.rs | 934 ++++++++++++++++++------------------- 1 file changed, 467 insertions(+), 467 deletions(-) diff --git a/maple-core/src/reactive.rs b/maple-core/src/reactive.rs index df22e83ed..d21d2c8c9 100644 --- a/maple-core/src/reactive.rs +++ b/maple-core/src/reactive.rs @@ -1,467 +1,467 @@ -//! Reactive primitives. - -use std::cell::Cell; -use std::cell::RefCell; -use std::ops::Deref; -use std::ptr::NonNull; -use std::rc::Rc; - -/// Returned by functions that provide a handle to access state. -pub struct StateHandle(Rc>>); - -impl StateHandle { - /// Get the current value of the state. - pub fn get(&self) -> Rc { - // if inside an effect, add this signal to dependency list - DEPENDENCIES.with(|dependencies| { - if dependencies.borrow().is_some() { - let signal = self.0.clone(); - let handler = HANDLER.with(|handler| handler.borrow().as_ref().unwrap().clone()); - - dependencies - .borrow_mut() - .as_mut() - .unwrap() - .push(Box::new(move || { - signal.borrow_mut().observe(handler.clone()) - })); - } - }); - - self.get_untracked() - } - - /// Get the current value of the state, without tracking this as a dependency if inside a - /// reactive context. - /// - /// # Example - /// - /// ``` - /// use maple_core::prelude::*; - /// - /// let state = Signal::new(1); - /// - /// let double = create_memo({ - /// let state = state.clone(); - /// move || *state.get_untracked() * 2 - /// }); - /// - /// assert_eq!(*double.get(), 2); - /// - /// state.set(2); - /// // double value should still be old value because state was untracked - /// assert_eq!(*double.get(), 2); - /// ``` - pub fn get_untracked(&self) -> Rc { - self.0.borrow().inner.clone() - } -} - -impl Clone for StateHandle { - fn clone(&self) -> Self { - Self(self.0.clone()) - } -} - -/// State that can be set. -pub struct Signal(StateHandle); - -impl Signal { - /// Creates a new signal. - /// - /// # Examples - /// - /// ``` - /// use maple_core::prelude::*; - /// - /// let state = Signal::new(0); - /// assert_eq!(*state.get(), 0); - /// - /// state.set(1); - /// assert_eq!(*state.get(), 1); - /// ``` - pub fn new(value: T) -> Self { - Self(StateHandle(Rc::new(RefCell::new(SignalInner::new(value))))) - } - - /// Set the current value of the state. - /// - /// This will notify and update any effects and memos that depend on this value. - pub fn set(&self, new_value: T) { - match self.0 .0.try_borrow_mut() { - Ok(mut signal) => signal.update(new_value), - // If the signal is already borrowed, that means it is borrowed in the getter, thus creating a cyclic dependency. - Err(_err) => panic!("cannot create cyclic dependency"), - } - self.0 .0.borrow().trigger_observers(); - } -} - -impl Deref for Signal { - type Target = StateHandle; - - fn deref(&self) -> &Self::Target { - &self.0 - } -} - -impl Clone for Signal { - fn clone(&self) -> Self { - Self(self.0.clone()) - } -} - -struct SignalInner { - inner: Rc, - observers: Vec>, -} - -impl SignalInner { - fn new(value: T) -> Self { - Self { - inner: Rc::new(value), - observers: Vec::new(), - } - } - - fn observe(&mut self, handler: Rc) { - // make sure handler is not already in self.observers - if self - .observers - .iter() - .find(|observer| { - observer.as_ref() as *const Computation == handler.as_ref() as *const Computation - /* do reference equality */ - }) - .is_none() - { - self.observers.push(handler); - } - } - - fn update(&mut self, new_value: T) { - self.inner = Rc::new(new_value); - } - - fn trigger_observers(&self) { - for observer in &self.observers { - observer.0(); - } - } -} - -/// A derived computation from a signal. -struct Computation(Box); - -thread_local! { - static HANDLER: RefCell>> = RefCell::new(None); - - /// To add the dependencies, iterate through functions and execute them. - static DEPENDENCIES: RefCell>>> = RefCell::new(None); -} - -/// Creates an effect on signals used inside the effect closure. -pub fn create_effect(effect: F) -where - F: Fn() + 'static, -{ - DEPENDENCIES.with(|dependencies| { - if dependencies.borrow().is_some() { - unimplemented!("nested dependencies are not supported") - } - - let effect = Rc::new(Computation(Box::new(effect))); - - *dependencies.borrow_mut() = Some(Vec::new()); - HANDLER.with(|handler| *handler.borrow_mut() = Some(effect.clone())); - - // run effect for the first time to attach all the dependencies - effect.0(); - - // attach dependencies - for dependency in dependencies.borrow().as_ref().unwrap() { - dependency(); - } - - // Reset dependencies for next effect hook - *dependencies.borrow_mut() = None; - }) -} - -/// Creates a memoized value from some signals. Also know as "derived stores". -pub fn create_memo(derived: F) -> StateHandle -where - F: Fn() -> Out + 'static, - Out: 'static, -{ - create_selector_with(derived, |_, _| false) -} - -/// Creates a memoized value from some signals. Also know as "derived stores". -/// Unlike [`create_memo`], this function will not notify dependents of a change if the output is the same. -/// That is why the output of the function must implement [`PartialEq`]. -/// -/// To specify a custom comparison function, use [`create_selector_with`]. -pub fn create_selector(derived: F) -> StateHandle -where - F: Fn() -> Out + 'static, - Out: PartialEq + 'static, -{ - create_selector_with(derived, PartialEq::eq) -} - -/// Creates a memoized value from some signals. Also know as "derived stores". -/// Unlike [`create_memo`], this function will not notify dependents of a change if the output is the same. -/// -/// It takes a comparison function to compare the old and new value, which returns `true` if they -/// are the same and `false` otherwise. -/// -/// To use the type's [`PartialEq`] implementation instead of a custom function, use -/// [`create_selector`]. -pub fn create_selector_with(derived: F, comparator: C) -> StateHandle -where - F: Fn() -> Out + 'static, - Out: 'static, - C: Fn(&Out, &Out) -> bool + 'static, -{ - enum EffectState { - Uninitialized(NonNull>>), - Initialized(Signal), - Temporary, - } - - let mut handle = None; - let state = Cell::new(EffectState::Uninitialized(NonNull::from(&mut handle))); - - create_effect(move || { - let memo = match state.replace(EffectState::Temporary) { - // We haven't been called yet, so initialize the signal. - EffectState::Uninitialized(mut handle_ptr) => { - let memo = Signal::new(derived()); - - // SAFETY: This is the first time the function is called, and so this pointer is - // still valid. - *unsafe { handle_ptr.as_mut() } = Some((*memo).clone()); - - memo - } - // We have been called; update the signal. - EffectState::Initialized(memo) => { - let new_value = derived(); - if !comparator(&memo.get_untracked(), &new_value) { - memo.set(new_value); - } - memo - } - EffectState::Temporary => unreachable!(), - }; - state.replace(EffectState::Initialized(memo)); - }); - - // By this time, the above closure will have been run once, setting the `handle` value. - handle.unwrap() -} - -#[cfg(test)] -mod tests { - use super::*; - - #[test] - fn signals() { - let state = Signal::new(0); - assert_eq!(*state.get(), 0); - - state.set(1); - assert_eq!(*state.get(), 1); - } - - #[test] - fn signal_composition() { - let state = Signal::new(0); - - let double = || *state.get() * 2; - - assert_eq!(double(), 0); - - state.set(1); - assert_eq!(double(), 2); - } - - #[test] - fn effects() { - let state = Signal::new(0); - - let double = Signal::new(-1); - - create_effect({ - let state = state.clone(); - let double = double.clone(); - move || { - double.set(*state.get() * 2); - } - }); - assert_eq!(*double.get(), 0); // calling create_effect should call the effect at least once - - state.set(1); - assert_eq!(*double.get(), 2); - state.set(2); - assert_eq!(*double.get(), 4); - } - - #[test] - #[should_panic(expected = "cannot create cyclic dependency")] - fn cyclic_effects_fail() { - let state = Signal::new(0); - - create_effect({ - let state = state.clone(); - move || { - state.set(*state.get() + 1); - } - }); - - state.set(1); - } - - #[test] - #[should_panic(expected = "cannot create cyclic dependency")] - fn cyclic_effects_fail_2() { - let state = Signal::new(0); - - create_effect({ - let state = state.clone(); - move || { - let value = *state.get(); - state.set(value + 1); - } - }); - - state.set(1); - } - - #[test] - fn effect_should_subscribe_once() { - let state = Signal::new(0); - - let counter = Signal::new(0); - create_effect({ - let state = state.clone(); - let counter = counter.clone(); - move || { - counter.set(*counter.get_untracked() + 1); - - // call state.get() twice but should subscribe once - state.get(); - state.get(); - } - }); - - assert_eq!(*counter.get(), 1); - - state.set(1); - assert_eq!(*counter.get(), 2); - } - - #[test] - fn memo() { - let state = Signal::new(0); - - let double = create_memo({ - let state = state.clone(); - move || *state.get() * 2 - }); - assert_eq!(*double.get(), 0); - - state.set(1); - assert_eq!(*double.get(), 2); - - state.set(2); - assert_eq!(*double.get(), 4); - } - - #[test] - /// Make sure value is memoized rather than executed on demand. - fn memo_only_run_once() { - let state = Signal::new(0); - - let counter = Signal::new(0); - let double = create_memo({ - let state = state.clone(); - let counter = counter.clone(); - move || { - counter.set(*counter.get_untracked() + 1); - - *state.get() * 2 - } - }); - assert_eq!(*counter.get(), 1); // once for calculating initial derived state - - state.set(2); - assert_eq!(*counter.get(), 2); - assert_eq!(*double.get(), 4); - assert_eq!(*counter.get(), 2); // should still be 2 after access - } - - #[test] - fn dependency_on_memo() { - let state = Signal::new(0); - - let double = create_memo({ - let state = state.clone(); - move || *state.get() * 2 - }); - - let quadruple = create_memo(move || *double.get() * 2); - - assert_eq!(*quadruple.get(), 0); - - state.set(1); - assert_eq!(*quadruple.get(), 4); - } - - #[test] - fn untracked_memo() { - let state = Signal::new(1); - - let double = create_memo({ - let state = state.clone(); - move || *state.get_untracked() * 2 - }); - - assert_eq!(*double.get(), 2); - - state.set(2); - assert_eq!(*double.get(), 2); // double value should still be true because state.get() was inside untracked - } - - #[test] - fn selector() { - let state = Signal::new(0); - - let double = create_selector({ - let state = state.clone(); - move || *state.get() * 2 - }); - - let counter = Signal::new(0); - create_effect({ - let counter = counter.clone(); - let double = double.clone(); - move || { - counter.set(*counter.get_untracked() + 1); - - double.get(); - } - }); - assert_eq!(*double.get(), 0); - assert_eq!(*counter.get(), 1); - - state.set(0); - assert_eq!(*double.get(), 0); - assert_eq!(*counter.get(), 1); // calling set_state should not trigger the effect - - state.set(2); - assert_eq!(*double.get(), 4); - assert_eq!(*counter.get(), 2); - } -} +//! Reactive primitives. + +use std::cell::Cell; +use std::cell::RefCell; +use std::ops::Deref; +use std::ptr::NonNull; +use std::rc::Rc; + +/// Returned by functions that provide a handle to access state. +pub struct StateHandle(Rc>>); + +impl StateHandle { + /// Get the current value of the state. + pub fn get(&self) -> Rc { + // if inside an effect, add this signal to dependency list + DEPENDENCIES.with(|dependencies| { + if dependencies.borrow().is_some() { + let signal = self.0.clone(); + let handler = HANDLER.with(|handler| handler.borrow().as_ref().unwrap().clone()); + + dependencies + .borrow_mut() + .as_mut() + .unwrap() + .push(Box::new(move || { + signal.borrow_mut().observe(handler.clone()) + })); + } + }); + + self.get_untracked() + } + + /// Get the current value of the state, without tracking this as a dependency if inside a + /// reactive context. + /// + /// # Example + /// + /// ``` + /// use maple_core::prelude::*; + /// + /// let state = Signal::new(1); + /// + /// let double = create_memo({ + /// let state = state.clone(); + /// move || *state.get_untracked() * 2 + /// }); + /// + /// assert_eq!(*double.get(), 2); + /// + /// state.set(2); + /// // double value should still be old value because state was untracked + /// assert_eq!(*double.get(), 2); + /// ``` + pub fn get_untracked(&self) -> Rc { + self.0.borrow().inner.clone() + } +} + +impl Clone for StateHandle { + fn clone(&self) -> Self { + Self(self.0.clone()) + } +} + +/// State that can be set. +pub struct Signal(StateHandle); + +impl Signal { + /// Creates a new signal. + /// + /// # Examples + /// + /// ``` + /// use maple_core::prelude::*; + /// + /// let state = Signal::new(0); + /// assert_eq!(*state.get(), 0); + /// + /// state.set(1); + /// assert_eq!(*state.get(), 1); + /// ``` + pub fn new(value: T) -> Self { + Self(StateHandle(Rc::new(RefCell::new(SignalInner::new(value))))) + } + + /// Set the current value of the state. + /// + /// This will notify and update any effects and memos that depend on this value. + pub fn set(&self, new_value: T) { + match self.0 .0.try_borrow_mut() { + Ok(mut signal) => signal.update(new_value), + // If the signal is already borrowed, that means it is borrowed in the getter, thus creating a cyclic dependency. + Err(_err) => panic!("cannot create cyclic dependency"), + } + self.0 .0.borrow().trigger_observers(); + } +} + +impl Deref for Signal { + type Target = StateHandle; + + fn deref(&self) -> &Self::Target { + &self.0 + } +} + +impl Clone for Signal { + fn clone(&self) -> Self { + Self(self.0.clone()) + } +} + +struct SignalInner { + inner: Rc, + observers: Vec>, +} + +impl SignalInner { + fn new(value: T) -> Self { + Self { + inner: Rc::new(value), + observers: Vec::new(), + } + } + + fn observe(&mut self, handler: Rc) { + // make sure handler is not already in self.observers + if self + .observers + .iter() + .find(|observer| { + observer.as_ref() as *const Computation == handler.as_ref() as *const Computation + /* do reference equality */ + }) + .is_none() + { + self.observers.push(handler); + } + } + + fn update(&mut self, new_value: T) { + self.inner = Rc::new(new_value); + } + + fn trigger_observers(&self) { + for observer in &self.observers { + observer.0(); + } + } +} + +/// A derived computation from a signal. +struct Computation(Box); + +thread_local! { + static HANDLER: RefCell>> = RefCell::new(None); + + /// To add the dependencies, iterate through functions and execute them. + static DEPENDENCIES: RefCell>>> = RefCell::new(None); +} + +/// Creates an effect on signals used inside the effect closure. +pub fn create_effect(effect: F) +where + F: Fn() + 'static, +{ + DEPENDENCIES.with(|dependencies| { + if dependencies.borrow().is_some() { + unimplemented!("nested dependencies are not supported") + } + + let effect = Rc::new(Computation(Box::new(effect))); + + *dependencies.borrow_mut() = Some(Vec::new()); + HANDLER.with(|handler| *handler.borrow_mut() = Some(effect.clone())); + + // run effect for the first time to attach all the dependencies + effect.0(); + + // attach dependencies + for dependency in dependencies.borrow().as_ref().unwrap() { + dependency(); + } + + // Reset dependencies for next effect hook + *dependencies.borrow_mut() = None; + }) +} + +/// Creates a memoized value from some signals. Also know as "derived stores". +pub fn create_memo(derived: F) -> StateHandle +where + F: Fn() -> Out + 'static, + Out: 'static, +{ + create_selector_with(derived, |_, _| false) +} + +/// Creates a memoized value from some signals. Also know as "derived stores". +/// Unlike [`create_memo`], this function will not notify dependents of a change if the output is the same. +/// That is why the output of the function must implement [`PartialEq`]. +/// +/// To specify a custom comparison function, use [`create_selector_with`]. +pub fn create_selector(derived: F) -> StateHandle +where + F: Fn() -> Out + 'static, + Out: PartialEq + 'static, +{ + create_selector_with(derived, PartialEq::eq) +} + +/// Creates a memoized value from some signals. Also know as "derived stores". +/// Unlike [`create_memo`], this function will not notify dependents of a change if the output is the same. +/// +/// It takes a comparison function to compare the old and new value, which returns `true` if they +/// are the same and `false` otherwise. +/// +/// To use the type's [`PartialEq`] implementation instead of a custom function, use +/// [`create_selector`]. +pub fn create_selector_with(derived: F, comparator: C) -> StateHandle +where + F: Fn() -> Out + 'static, + Out: 'static, + C: Fn(&Out, &Out) -> bool + 'static, +{ + enum EffectState { + Uninitialized(NonNull>>), + Initialized(Signal), + Temporary, + } + + let mut handle = None; + let state = Cell::new(EffectState::Uninitialized(NonNull::from(&mut handle))); + + create_effect(move || { + let memo = match state.replace(EffectState::Temporary) { + // We haven't been called yet, so initialize the signal. + EffectState::Uninitialized(mut handle_ptr) => { + let memo = Signal::new(derived()); + + // SAFETY: This is the first time the function is called, and so this pointer is + // still valid. + *unsafe { handle_ptr.as_mut() } = Some((*memo).clone()); + + memo + } + // We have been called; update the signal. + EffectState::Initialized(memo) => { + let new_value = derived(); + if !comparator(&memo.get_untracked(), &new_value) { + memo.set(new_value); + } + memo + } + EffectState::Temporary => unreachable!(), + }; + state.replace(EffectState::Initialized(memo)); + }); + + // By this time, the above closure will have been run once, setting the `handle` value. + handle.unwrap() +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn signals() { + let state = Signal::new(0); + assert_eq!(*state.get(), 0); + + state.set(1); + assert_eq!(*state.get(), 1); + } + + #[test] + fn signal_composition() { + let state = Signal::new(0); + + let double = || *state.get() * 2; + + assert_eq!(double(), 0); + + state.set(1); + assert_eq!(double(), 2); + } + + #[test] + fn effects() { + let state = Signal::new(0); + + let double = Signal::new(-1); + + create_effect({ + let state = state.clone(); + let double = double.clone(); + move || { + double.set(*state.get() * 2); + } + }); + assert_eq!(*double.get(), 0); // calling create_effect should call the effect at least once + + state.set(1); + assert_eq!(*double.get(), 2); + state.set(2); + assert_eq!(*double.get(), 4); + } + + #[test] + #[should_panic(expected = "cannot create cyclic dependency")] + fn cyclic_effects_fail() { + let state = Signal::new(0); + + create_effect({ + let state = state.clone(); + move || { + state.set(*state.get() + 1); + } + }); + + state.set(1); + } + + #[test] + #[should_panic(expected = "cannot create cyclic dependency")] + fn cyclic_effects_fail_2() { + let state = Signal::new(0); + + create_effect({ + let state = state.clone(); + move || { + let value = *state.get(); + state.set(value + 1); + } + }); + + state.set(1); + } + + #[test] + fn effect_should_subscribe_once() { + let state = Signal::new(0); + + let counter = Signal::new(0); + create_effect({ + let state = state.clone(); + let counter = counter.clone(); + move || { + counter.set(*counter.get_untracked() + 1); + + // call state.get() twice but should subscribe once + state.get(); + state.get(); + } + }); + + assert_eq!(*counter.get(), 1); + + state.set(1); + assert_eq!(*counter.get(), 2); + } + + #[test] + fn memo() { + let state = Signal::new(0); + + let double = create_memo({ + let state = state.clone(); + move || *state.get() * 2 + }); + assert_eq!(*double.get(), 0); + + state.set(1); + assert_eq!(*double.get(), 2); + + state.set(2); + assert_eq!(*double.get(), 4); + } + + #[test] + /// Make sure value is memoized rather than executed on demand. + fn memo_only_run_once() { + let state = Signal::new(0); + + let counter = Signal::new(0); + let double = create_memo({ + let state = state.clone(); + let counter = counter.clone(); + move || { + counter.set(*counter.get_untracked() + 1); + + *state.get() * 2 + } + }); + assert_eq!(*counter.get(), 1); // once for calculating initial derived state + + state.set(2); + assert_eq!(*counter.get(), 2); + assert_eq!(*double.get(), 4); + assert_eq!(*counter.get(), 2); // should still be 2 after access + } + + #[test] + fn dependency_on_memo() { + let state = Signal::new(0); + + let double = create_memo({ + let state = state.clone(); + move || *state.get() * 2 + }); + + let quadruple = create_memo(move || *double.get() * 2); + + assert_eq!(*quadruple.get(), 0); + + state.set(1); + assert_eq!(*quadruple.get(), 4); + } + + #[test] + fn untracked_memo() { + let state = Signal::new(1); + + let double = create_memo({ + let state = state.clone(); + move || *state.get_untracked() * 2 + }); + + assert_eq!(*double.get(), 2); + + state.set(2); + assert_eq!(*double.get(), 2); // double value should still be true because state.get() was inside untracked + } + + #[test] + fn selector() { + let state = Signal::new(0); + + let double = create_selector({ + let state = state.clone(); + move || *state.get() * 2 + }); + + let counter = Signal::new(0); + create_effect({ + let counter = counter.clone(); + let double = double.clone(); + move || { + counter.set(*counter.get_untracked() + 1); + + double.get(); + } + }); + assert_eq!(*double.get(), 0); + assert_eq!(*counter.get(), 1); + + state.set(0); + assert_eq!(*double.get(), 0); + assert_eq!(*counter.get(), 1); // calling set_state should not trigger the effect + + state.set(2); + assert_eq!(*double.get(), 4); + assert_eq!(*counter.get(), 2); + } +} From ae3d19f68b950911438303627eeb0930ff79819d Mon Sep 17 00:00:00 2001 From: Kestrer Date: Mon, 8 Mar 2021 19:47:19 +0000 Subject: [PATCH 3/4] Add Signal::handle --- examples/components/src/main.rs | 4 ++-- maple-core/src/reactive.rs | 9 ++++++++- 2 files changed, 10 insertions(+), 3 deletions(-) diff --git a/examples/components/src/main.rs b/examples/components/src/main.rs index c058b889e..c3a1e8e6f 100644 --- a/examples/components/src/main.rs +++ b/examples/components/src/main.rs @@ -33,8 +33,8 @@ fn main() { # "Component demo" } - MyComponent((*state).clone()) - MyComponent((*state).clone()) + MyComponent(state.handle()) + MyComponent(state.handle()) button(on:click=increment) { # "Increment" diff --git a/maple-core/src/reactive.rs b/maple-core/src/reactive.rs index d21d2c8c9..cd0c89dea 100644 --- a/maple-core/src/reactive.rs +++ b/maple-core/src/reactive.rs @@ -95,6 +95,13 @@ impl Signal { } self.0 .0.borrow().trigger_observers(); } + + /// Get the [`StateHandle`] associated with this signal. + /// + /// This is a shortcut for `(*signal).clone()`. + pub fn handle(&self) -> StateHandle { + self.0.clone() + } } impl Deref for Signal { @@ -241,7 +248,7 @@ where // SAFETY: This is the first time the function is called, and so this pointer is // still valid. - *unsafe { handle_ptr.as_mut() } = Some((*memo).clone()); + *unsafe { handle_ptr.as_mut() } = Some(memo.handle()); memo } From e09526152d5a7a8036d1428c46f19ebaf23233aa Mon Sep 17 00:00:00 2001 From: Kestrer Date: Mon, 8 Mar 2021 19:59:03 +0000 Subject: [PATCH 4/4] Pass handler as a callback to the dependencies --- maple-core/src/reactive.rs | 81 +++++++++++++++++--------------------- 1 file changed, 36 insertions(+), 45 deletions(-) diff --git a/maple-core/src/reactive.rs b/maple-core/src/reactive.rs index cd0c89dea..c6191ea4c 100644 --- a/maple-core/src/reactive.rs +++ b/maple-core/src/reactive.rs @@ -1,9 +1,7 @@ //! Reactive primitives. -use std::cell::Cell; use std::cell::RefCell; use std::ops::Deref; -use std::ptr::NonNull; use std::rc::Rc; /// Returned by functions that provide a handle to access state. @@ -16,13 +14,12 @@ impl StateHandle { DEPENDENCIES.with(|dependencies| { if dependencies.borrow().is_some() { let signal = self.0.clone(); - let handler = HANDLER.with(|handler| handler.borrow().as_ref().unwrap().clone()); dependencies .borrow_mut() .as_mut() .unwrap() - .push(Box::new(move || { + .push(Box::new(move |handler| { signal.borrow_mut().observe(handler.clone()) })); } @@ -102,6 +99,11 @@ impl Signal { pub fn handle(&self) -> StateHandle { self.0.clone() } + + /// Convert this signal into its underlying handle. + pub fn into_handle(self) -> StateHandle { + self.0 + } } impl Deref for Signal { @@ -160,38 +162,48 @@ impl SignalInner { /// A derived computation from a signal. struct Computation(Box); -thread_local! { - static HANDLER: RefCell>> = RefCell::new(None); +type Dependency = Box)>; +thread_local! { /// To add the dependencies, iterate through functions and execute them. - static DEPENDENCIES: RefCell>>> = RefCell::new(None); + static DEPENDENCIES: RefCell>> = RefCell::new(None); } /// Creates an effect on signals used inside the effect closure. -pub fn create_effect(effect: F) -where - F: Fn() + 'static, -{ +/// +/// Unlike [`create_effect`], this will allow the closure to run different code upon first +/// execution, so it can return a value. +fn create_effect_initial(initial: impl FnOnce() -> (Rc, R)) -> R { DEPENDENCIES.with(|dependencies| { if dependencies.borrow().is_some() { unimplemented!("nested dependencies are not supported") } - let effect = Rc::new(Computation(Box::new(effect))); - *dependencies.borrow_mut() = Some(Vec::new()); - HANDLER.with(|handler| *handler.borrow_mut() = Some(effect.clone())); // run effect for the first time to attach all the dependencies - effect.0(); + let (effect, ret) = initial(); // attach dependencies for dependency in dependencies.borrow().as_ref().unwrap() { - dependency(); + dependency(&effect); } // Reset dependencies for next effect hook *dependencies.borrow_mut() = None; + + ret + }) +} + +/// Creates an effect on signals used inside the effect closure. +pub fn create_effect(effect: F) +where + F: Fn() + 'static, +{ + create_effect_initial(move || { + effect(); + (Rc::new(Computation(Box::new(effect))), ()) }) } @@ -231,42 +243,21 @@ where Out: 'static, C: Fn(&Out, &Out) -> bool + 'static, { - enum EffectState { - Uninitialized(NonNull>>), - Initialized(Signal), - Temporary, - } + create_effect_initial(|| { + let memo = Signal::new(derived()); - let mut handle = None; - let state = Cell::new(EffectState::Uninitialized(NonNull::from(&mut handle))); - - create_effect(move || { - let memo = match state.replace(EffectState::Temporary) { - // We haven't been called yet, so initialize the signal. - EffectState::Uninitialized(mut handle_ptr) => { - let memo = Signal::new(derived()); - - // SAFETY: This is the first time the function is called, and so this pointer is - // still valid. - *unsafe { handle_ptr.as_mut() } = Some(memo.handle()); - - memo - } - // We have been called; update the signal. - EffectState::Initialized(memo) => { + let effect = Rc::new(Computation(Box::new({ + let memo = memo.clone(); + move || { let new_value = derived(); if !comparator(&memo.get_untracked(), &new_value) { memo.set(new_value); } - memo } - EffectState::Temporary => unreachable!(), - }; - state.replace(EffectState::Initialized(memo)); - }); + }))); - // By this time, the above closure will have been run once, setting the `handle` value. - handle.unwrap() + (effect, memo.into_handle()) + }) } #[cfg(test)]