Forráskód Böngészése

Merge pull request #859 from Demonthos/use-shared-state-futures

Make use_shared_state usable in static futures
Jon Kelley 2 éve
szülő
commit
5b61cbb9a0

+ 1 - 1
docs/guide/examples/hooks_composed.rs

@@ -7,7 +7,7 @@ fn main() {}
 struct AppSettings {}
 
 // ANCHOR: wrap_context
-fn use_settings(cx: &ScopeState) -> UseSharedState<AppSettings> {
+fn use_settings(cx: &ScopeState) -> &UseSharedState<AppSettings> {
     use_shared_state::<AppSettings>(cx).expect("App settings not provided")
 }
 // ANCHOR_END: wrap_context

+ 1 - 0
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" }

+ 137 - 98
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,
@@ -9,8 +9,8 @@ use std::{
 type ProvidedState<T> = Rc<RefCell<ProvidedStateInner<T>>>;
 
 // Tracks all the subscribers to a shared State
-pub struct ProvidedStateInner<T> {
-    value: Rc<RefCell<T>>,
+pub(crate) struct ProvidedStateInner<T> {
+    value: T,
     notify_any: Arc<dyn Fn(ScopeId)>,
     consumers: HashSet<ScopeId>,
 }
@@ -21,14 +21,6 @@ impl<T> ProvidedStateInner<T> {
             (self.notify_any)(*consumer);
         }
     }
-
-    pub fn write(&self) -> RefMut<T> {
-        self.value.borrow_mut()
-    }
-
-    pub fn read(&self) -> Ref<T> {
-        self.value.borrow()
-    }
 }
 
 /// This hook provides some relatively light ergonomics around shared state.
@@ -38,18 +30,57 @@ impl<T> ProvidedStateInner<T> {
 ///
 /// # Example
 ///
-/// ## Provider
-///
-/// ```rust, ignore
-///
-///
-/// ```
-///
-/// ## Consumer
-///
-/// ```rust, ignore
-///
-///
+/// ```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::<Theme>(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"
+///         }
+///         Child{}
+///     }
+/// }
+///
+/// // Consumer
+/// fn Child<'a>(cx: Scope<'a>) -> Element<'a> {
+///     let theme = use_shared_state::<Theme>(cx).unwrap();
+///     let current_theme = *theme.read();
+///
+///     render! {
+///         match &*theme.read() {
+///             Theme::Dark => {
+///                 "Dark mode"
+///             }
+///             Theme::Light => {
+///                 "Light mode"
+///             }
+///         }
+///     }
+/// }
 /// ```
 ///
 /// # How it works
@@ -57,118 +88,126 @@ impl<T> ProvidedStateInner<T> {
 /// 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<T: 'static>(cx: &ScopeState) -> Option<UseSharedState<T>> {
-    let state = cx.use_hook(|| {
+pub fn use_shared_state<T: 'static>(cx: &ScopeState) -> Option<&UseSharedState<T>> {
+    let state: &Option<UseSharedStateOwner<T>> = &*cx.use_hook(move || {
         let scope_id = cx.scope_id();
-        let root = cx.consume_context::<ProvidedState<T>>();
+        let root = cx.consume_context::<ProvidedState<T>>()?;
 
-        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 state = UseSharedState { inner: 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<T: 'static> {
-    root: Option<ProvidedState<T>>,
-    value: Option<Rc<RefCell<T>>>,
+/// This wrapper detects when the hook is dropped and will unsubscribe when the component is unmounted
+struct UseSharedStateOwner<T> {
+    state: UseSharedState<T>,
     scope_id: ScopeId,
-    needs_notification: Cell<bool>,
 }
-impl<T> Drop for SharedStateInner<T> {
+
+impl<T> Drop for UseSharedStateOwner<T> {
     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.inner.borrow_mut();
+        root.consumers.remove(&self.scope_id);
     }
 }
 
-pub struct UseSharedState<'a, T: 'static> {
-    pub(crate) cx: &'a ScopeState,
-    pub(crate) value: &'a Rc<RefCell<T>>,
-    pub(crate) root: &'a Rc<RefCell<ProvidedStateInner<T>>>,
-    pub(crate) needs_notification: &'a Cell<bool>,
+/// State that is shared between components through the context system
+pub struct UseSharedState<T> {
+    pub(crate) inner: Rc<RefCell<ProvidedStateInner<T>>>,
 }
 
-impl<'a, T: 'static> UseSharedState<'a, T> {
-    pub fn read(&self) -> Ref<'_, T> {
-        self.value.borrow()
+impl<T> UseSharedState<T> {
+    /// 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();
     }
 
-    pub fn notify_consumers(self) {
-        if !self.needs_notification.get() {
-            self.root.borrow_mut().notify_consumers();
-            self.needs_notification.set(true);
-        }
-    }
-
-    pub fn read_write(&self) -> (Ref<'_, T>, &Self) {
-        (self.read(), self)
+    /// Read the shared value
+    pub fn read(&self) -> Ref<'_, T> {
+        Ref::map(self.inner.borrow(), |inner| &inner.value)
     }
 
     /// 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> {
-        self.cx.needs_update();
-        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<RefCell<ProvidedStateInner<T>>> {
-        self.root.clone()
+        RefMut::map(self.inner.borrow_mut(), |inner| &mut inner.value)
     }
 }
 
-impl<T> Copy for UseSharedState<'_, T> {}
-impl<'a, T> Clone for UseSharedState<'a, T>
-where
-    T: 'static,
-{
+impl<T> Clone for UseSharedState<T> {
     fn clone(&self) -> Self {
-        UseSharedState {
-            cx: self.cx,
-            value: self.value,
-            root: self.root,
-            needs_notification: self.needs_notification,
+        Self {
+            inner: self.inner.clone(),
         }
     }
 }
 
-/// Provide some state for components down the hierarchy to consume without having to drill props.
+impl<T: PartialEq> PartialEq for UseSharedState<T> {
+    fn eq(&self, other: &Self) -> bool {
+        let first = self.inner.borrow();
+        let second = other.inner.borrow();
+        first.value == second.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::<Theme>(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<T: 'static>(cx: &ScopeState, f: impl FnOnce() -> T) {
     cx.use_hook(|| {
         let state: ProvidedState<T> = Rc::new(RefCell::new(ProvidedStateInner {
-            value: Rc::new(RefCell::new(f())),
+            value: f(),
             notify_any: cx.schedule_update_any(),
             consumers: HashSet::new(),
         }));