From f5c058a2ebaa49bb8dfe902fcfbe799955d1b2c5 Mon Sep 17 00:00:00 2001 From: Evan Almloff Date: Thu, 2 Mar 2023 11:42:02 -0600 Subject: [PATCH 1/5] make use_shared_state usable in static futures --- packages/hooks/Cargo.toml | 1 + packages/hooks/src/use_shared_state.rs | 187 ++++++++++++++++--------- 2 files changed, 120 insertions(+), 68 deletions(-) diff --git a/packages/hooks/Cargo.toml b/packages/hooks/Cargo.toml index c1b31ea65..c0e1ea783 100644 --- a/packages/hooks/Cargo.toml +++ b/packages/hooks/Cargo.toml @@ -20,3 +20,4 @@ log = "0.4" [dev-dependencies] futures-util = { version = "0.3", default-features = false } dioxus-core = { path = "../../packages/core", version = "^0.3.0" } +dioxus = { path = "../../packages/dioxus", version = "^0.3.0" } diff --git a/packages/hooks/src/use_shared_state.rs b/packages/hooks/src/use_shared_state.rs index a355ae42a..173bfbb71 100644 --- a/packages/hooks/src/use_shared_state.rs +++ b/packages/hooks/src/use_shared_state.rs @@ -1,6 +1,6 @@ use dioxus_core::{ScopeId, ScopeState}; use std::{ - cell::{Cell, Ref, RefCell, RefMut}, + cell::{Ref, RefCell, RefMut}, collections::HashSet, rc::Rc, sync::Arc, @@ -38,18 +38,57 @@ impl ProvidedStateInner { /// /// # Example /// -/// ## Provider +/// ```rust +/// # use dioxus::prelude::*; +/// # +/// # fn app(cx: Scope) -> Element { +/// # render! { +/// # Parent{} +/// # } +/// # } /// -/// ```rust, ignore +/// #[derive(Clone, Copy)] +/// enum Theme { +/// Light, +/// Dark, +/// } /// +/// // Provider +/// fn Parent<'a>(cx: Scope<'a>) -> Element<'a> { +/// use_shared_state_provider(cx, || Theme::Dark); +/// let theme = use_shared_state::(cx).unwrap(); /// -/// ``` -/// -/// ## Consumer -/// -/// ```rust, ignore +/// render! { +/// button{ +/// onclick: move |_| { +/// let current_theme = *theme.read(); +/// *theme.write() = match current_theme { +/// Theme::Dark => Theme::Light, +/// Theme::Light => Theme::Dark, +/// }; +/// }, +/// "Change theme" +/// } +/// Child{} +/// } +/// } /// +/// // Consumer +/// fn Child<'a>(cx: Scope<'a>) -> Element<'a> { +/// let theme = use_shared_state::(cx).unwrap(); +/// let current_theme = *theme.read(); /// +/// render! { +/// match &*theme.read() { +/// Theme::Dark => { +/// "Dark mode" +/// } +/// Theme::Light => { +/// "Light mode" +/// } +/// } +/// } +/// } /// ``` /// /// # How it works @@ -57,72 +96,46 @@ impl ProvidedStateInner { /// Any time a component calls `write`, every consumer of the state will be notified - excluding the provider. /// /// Right now, there is not a distinction between read-only and write-only, so every consumer will be notified. -/// -/// -/// -pub fn use_shared_state(cx: &ScopeState) -> Option> { - let state = cx.use_hook(|| { +pub fn use_shared_state(cx: &ScopeState) -> Option<&UseSharedState> { + let state: &Option> = &*cx.use_hook(move || { let scope_id = cx.scope_id(); - let root = cx.consume_context::>(); + let root = cx.consume_context::>()?; - if let Some(root) = root.as_ref() { - root.borrow_mut().consumers.insert(scope_id); - } + root.borrow_mut().consumers.insert(scope_id); - let value = root.as_ref().map(|f| f.borrow().value.clone()); - SharedStateInner { - root, - value, - scope_id, - needs_notification: Cell::new(false), - } + let value = root.borrow().value.clone(); + let state = UseSharedState { value, root }; + let owner = UseSharedStateOwner { state, scope_id }; + Some(owner) }); - - state.needs_notification.set(false); - match (&state.value, &state.root) { - (Some(value), Some(root)) => Some(UseSharedState { - cx, - value, - root, - needs_notification: &state.needs_notification, - }), - _ => None, - } + state.as_ref().map(|s| &s.state) } -struct SharedStateInner { - root: Option>, - value: Option>>, +struct UseSharedStateOwner { + state: UseSharedState, scope_id: ScopeId, - needs_notification: Cell, } -impl Drop for SharedStateInner { + +impl Drop for UseSharedStateOwner { fn drop(&mut self) { - // we need to unsubscribe when our component is unounted - if let Some(root) = &self.root { - let mut root = root.borrow_mut(); - root.consumers.remove(&self.scope_id); - } + // we need to unsubscribe when our component is unmounted + let mut root = self.state.root.borrow_mut(); + root.consumers.remove(&self.scope_id); } } -pub struct UseSharedState<'a, T: 'static> { - pub(crate) cx: &'a ScopeState, - pub(crate) value: &'a Rc>, - pub(crate) root: &'a Rc>>, - pub(crate) needs_notification: &'a Cell, +pub struct UseSharedState { + pub(crate) value: Rc>, + pub(crate) root: Rc>>, } -impl<'a, T: 'static> UseSharedState<'a, T> { +impl UseSharedState { pub fn read(&self) -> Ref<'_, T> { self.value.borrow() } - pub fn notify_consumers(self) { - if !self.needs_notification.get() { - self.root.borrow_mut().notify_consumers(); - self.needs_notification.set(true); - } + pub fn notify_consumers(&self) { + self.root.borrow_mut().notify_consumers(); } pub fn read_write(&self) -> (Ref<'_, T>, &Self) { @@ -134,7 +147,6 @@ impl<'a, T: 'static> UseSharedState<'a, T> { /// /// TODO: We prevent unncessary notifications only in the hook, but we should figure out some more global lock pub fn write(&self) -> RefMut<'_, T> { - self.cx.needs_update(); self.notify_consumers(); self.value.borrow_mut() } @@ -149,22 +161,61 @@ impl<'a, T: 'static> UseSharedState<'a, T> { } } -impl Copy for UseSharedState<'_, T> {} -impl<'a, T> Clone for UseSharedState<'a, T> -where - T: 'static, -{ +impl Clone for UseSharedState { fn clone(&self) -> Self { - UseSharedState { - cx: self.cx, - value: self.value, - root: self.root, - needs_notification: self.needs_notification, + Self { + value: self.value.clone(), + root: self.root.clone(), } } } -/// Provide some state for components down the hierarchy to consume without having to drill props. +impl PartialEq for UseSharedState { + fn eq(&self, other: &Self) -> bool { + self.value == other.value + } +} + +/// Provide some state for components down the hierarchy to consume without having to drill props. See [`use_shared_state`] to consume the state +/// +/// +/// # Example +/// +/// ```rust +/// # use dioxus::prelude::*; +/// # +/// # fn app(cx: Scope) -> Element { +/// # render! { +/// # Parent{} +/// # } +/// # } +/// +/// #[derive(Clone, Copy)] +/// enum Theme { +/// Light, +/// Dark, +/// } +/// +/// // Provider +/// fn Parent<'a>(cx: Scope<'a>) -> Element<'a> { +/// use_shared_state_provider(cx, || Theme::Dark); +/// let theme = use_shared_state::(cx).unwrap(); +/// +/// render! { +/// button{ +/// onclick: move |_| { +/// let current_theme = *theme.read(); +/// *theme.write() = match current_theme { +/// Theme::Dark => Theme::Light, +/// Theme::Light => Theme::Dark, +/// }; +/// }, +/// "Change theme" +/// } +/// // Children components that consume the state... +/// } +/// } +/// ``` pub fn use_shared_state_provider(cx: &ScopeState, f: impl FnOnce() -> T) { cx.use_hook(|| { let state: ProvidedState = Rc::new(RefCell::new(ProvidedStateInner { From d23848193686b8ca9fb6b1820ccac0e18b9c0fde Mon Sep 17 00:00:00 2001 From: Evan Almloff Date: Thu, 2 Mar 2023 11:54:21 -0600 Subject: [PATCH 2/5] reduce indirection and remove some unnecessary methods --- packages/hooks/src/use_shared_state.rs | 50 +++++++++----------------- 1 file changed, 17 insertions(+), 33 deletions(-) diff --git a/packages/hooks/src/use_shared_state.rs b/packages/hooks/src/use_shared_state.rs index 173bfbb71..8e0e813c8 100644 --- a/packages/hooks/src/use_shared_state.rs +++ b/packages/hooks/src/use_shared_state.rs @@ -9,8 +9,8 @@ use std::{ type ProvidedState = Rc>>; // Tracks all the subscribers to a shared State -pub struct ProvidedStateInner { - value: Rc>, +pub(crate) struct ProvidedStateInner { + value: T, notify_any: Arc, consumers: HashSet, } @@ -21,14 +21,6 @@ impl ProvidedStateInner { (self.notify_any)(*consumer); } } - - pub fn write(&self) -> RefMut { - self.value.borrow_mut() - } - - pub fn read(&self) -> Ref { - self.value.borrow() - } } /// This hook provides some relatively light ergonomics around shared state. @@ -103,8 +95,7 @@ pub fn use_shared_state(cx: &ScopeState) -> Option<&UseSharedState { impl Drop for UseSharedStateOwner { fn drop(&mut self) { // we need to unsubscribe when our component is unmounted - let mut root = self.state.root.borrow_mut(); + let mut root = self.state.inner.borrow_mut(); root.consumers.remove(&self.scope_id); } } pub struct UseSharedState { - pub(crate) value: Rc>, - pub(crate) root: Rc>>, + pub(crate) inner: Rc>>, } impl UseSharedState { - pub fn read(&self) -> Ref<'_, T> { - self.value.borrow() - } - pub fn notify_consumers(&self) { - self.root.borrow_mut().notify_consumers(); + self.inner.borrow_mut().notify_consumers(); } - pub fn read_write(&self) -> (Ref<'_, T>, &Self) { - (self.read(), self) + pub fn read(&self) -> Ref<'_, T> { + Ref::map(self.inner.borrow(), |inner| &inner.value) } /// Calling "write" will force the component to re-render @@ -147,32 +133,30 @@ impl UseSharedState { /// /// TODO: We prevent unncessary notifications only in the hook, but we should figure out some more global lock pub fn write(&self) -> RefMut<'_, T> { - self.notify_consumers(); - self.value.borrow_mut() + let mut value = self.inner.borrow_mut(); + value.notify_consumers(); + RefMut::map(value, |inner| &mut inner.value) } /// Allows the ability to write the value without forcing a re-render pub fn write_silent(&self) -> RefMut<'_, T> { - self.value.borrow_mut() - } - - pub fn inner(&self) -> Rc>> { - self.root.clone() + RefMut::map(self.inner.borrow_mut(), |inner| &mut inner.value) } } impl Clone for UseSharedState { fn clone(&self) -> Self { Self { - value: self.value.clone(), - root: self.root.clone(), + inner: self.inner.clone(), } } } impl PartialEq for UseSharedState { fn eq(&self, other: &Self) -> bool { - self.value == other.value + let first = self.inner.borrow(); + let second = other.inner.borrow(); + first.value == second.value } } @@ -219,7 +203,7 @@ impl PartialEq for UseSharedState { pub fn use_shared_state_provider(cx: &ScopeState, f: impl FnOnce() -> T) { cx.use_hook(|| { let state: ProvidedState = Rc::new(RefCell::new(ProvidedStateInner { - value: Rc::new(RefCell::new(f())), + value: f(), notify_any: cx.schedule_update_any(), consumers: HashSet::new(), })); From 90b4dd806e1491414971dc3138a42f12887298c9 Mon Sep 17 00:00:00 2001 From: Evan Almloff Date: Thu, 2 Mar 2023 11:57:58 -0600 Subject: [PATCH 3/5] update docs example --- docs/guide/examples/hooks_composed.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/guide/examples/hooks_composed.rs b/docs/guide/examples/hooks_composed.rs index 5b1f5ebfe..801474aa3 100644 --- a/docs/guide/examples/hooks_composed.rs +++ b/docs/guide/examples/hooks_composed.rs @@ -7,7 +7,7 @@ fn main() {} struct AppSettings {} // ANCHOR: wrap_context -fn use_settings(cx: &ScopeState) -> UseSharedState { +fn use_settings(cx: &ScopeState) -> &UseSharedState { use_shared_state::(cx).expect("App settings not provided") } // ANCHOR_END: wrap_context From 88198fc47cd7608bd3e1ec3c415d11f6a67505b3 Mon Sep 17 00:00:00 2001 From: Evan Almloff Date: Thu, 2 Mar 2023 11:59:25 -0600 Subject: [PATCH 4/5] remove static bound on owner --- packages/hooks/src/use_shared_state.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/hooks/src/use_shared_state.rs b/packages/hooks/src/use_shared_state.rs index 8e0e813c8..a7afc619e 100644 --- a/packages/hooks/src/use_shared_state.rs +++ b/packages/hooks/src/use_shared_state.rs @@ -102,7 +102,7 @@ pub fn use_shared_state(cx: &ScopeState) -> Option<&UseSharedState { +struct UseSharedStateOwner { state: UseSharedState, scope_id: ScopeId, } From 2dd40ec202c0005a62fec2c6fedf032a682f6cee Mon Sep 17 00:00:00 2001 From: Evan Almloff Date: Thu, 2 Mar 2023 12:02:12 -0600 Subject: [PATCH 5/5] add docs to remaining items --- packages/hooks/src/use_shared_state.rs | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/packages/hooks/src/use_shared_state.rs b/packages/hooks/src/use_shared_state.rs index a7afc619e..eb836a211 100644 --- a/packages/hooks/src/use_shared_state.rs +++ b/packages/hooks/src/use_shared_state.rs @@ -102,6 +102,7 @@ pub fn use_shared_state(cx: &ScopeState) -> Option<&UseSharedState { state: UseSharedState, scope_id: ScopeId, @@ -115,15 +116,18 @@ impl Drop for UseSharedStateOwner { } } +/// State that is shared between components through the context system pub struct UseSharedState { pub(crate) inner: Rc>>, } impl UseSharedState { + /// Notify all consumers of the state that it has changed. (This is called automatically when you call "write") pub fn notify_consumers(&self) { self.inner.borrow_mut().notify_consumers(); } + /// Read the shared value pub fn read(&self) -> Ref<'_, T> { Ref::map(self.inner.borrow(), |inner| &inner.value) } @@ -131,7 +135,7 @@ impl UseSharedState { /// Calling "write" will force the component to re-render /// /// - /// TODO: We prevent unncessary notifications only in the hook, but we should figure out some more global lock + // TODO: We prevent unncessary notifications only in the hook, but we should figure out some more global lock pub fn write(&self) -> RefMut<'_, T> { let mut value = self.inner.borrow_mut(); value.notify_consumers();