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

Make use_shared_state usable in static futures
This commit is contained in:
Jon Kelley authored and GitHub committed 2023-03-02 11:46:58 -08:00
commit 5b61cbb9a0
3 files changed
+135 -95

No files matched your search

+1 -1
View File
@@ -7,7 +7,7 @@ fn main() {}
struct AppSettings {} struct AppSettings {}
// ANCHOR: wrap_context // 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") use_shared_state::<AppSettings>(cx).expect("App settings not provided")
} }
// ANCHOR_END: wrap_context // ANCHOR_END: wrap_context
+1
View File
@@ -20,3 +20,4 @@ log = "0.4"
[dev-dependencies] [dev-dependencies]
futures-util = { version = "0.3", default-features = false } futures-util = { version = "0.3", default-features = false }
dioxus-core = { path = "../../packages/core", version = "^0.3.0" } dioxus-core = { path = "../../packages/core", version = "^0.3.0" }
dioxus = { path = "../../packages/dioxus", version = "^0.3.0" }
+133 -94
View File
@@ -1,6 +1,6 @@
use dioxus_core::{ScopeId, ScopeState}; use dioxus_core::{ScopeId, ScopeState};
use std::{ use std::{
cell::{Cell, Ref, RefCell, RefMut}, cell::{Ref, RefCell, RefMut},
collections::HashSet, collections::HashSet,
rc::Rc, rc::Rc,
sync::Arc, sync::Arc,
@@ -9,8 +9,8 @@ use std::{
type ProvidedState<T> = Rc<RefCell<ProvidedStateInner<T>>>; type ProvidedState<T> = Rc<RefCell<ProvidedStateInner<T>>>;
// Tracks all the subscribers to a shared State // Tracks all the subscribers to a shared State
pub struct ProvidedStateInner<T> { pub(crate) struct ProvidedStateInner<T> {
value: Rc<RefCell<T>>, value: T,
notify_any: Arc<dyn Fn(ScopeId)>, notify_any: Arc<dyn Fn(ScopeId)>,
consumers: HashSet<ScopeId>, consumers: HashSet<ScopeId>,
} }
@@ -21,14 +21,6 @@ impl<T> ProvidedStateInner<T> {
(self.notify_any)(*consumer); (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. /// This hook provides some relatively light ergonomics around shared state.
@@ -38,18 +30,57 @@ impl<T> ProvidedStateInner<T> {
/// ///
/// # Example /// # 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::<Theme>(cx).unwrap();
/// ///
/// ``` /// render! {
/// /// button{
/// ## Consumer /// onclick: move |_| {
/// /// let current_theme = *theme.read();
/// ```rust, ignore /// *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 /// # 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. /// 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. /// 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: &Option<UseSharedStateOwner<T>> = &*cx.use_hook(move || {
///
pub fn use_shared_state<T: 'static>(cx: &ScopeState) -> Option<UseSharedState<T>> {
let state = cx.use_hook(|| {
let scope_id = cx.scope_id(); 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()); let state = UseSharedState { inner: root };
SharedStateInner { let owner = UseSharedStateOwner { state, scope_id };
root, Some(owner)
value,
scope_id,
needs_notification: Cell::new(false),
}
}); });
state.as_ref().map(|s| &s.state)
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,
}
} }
struct SharedStateInner<T: 'static> { /// This wrapper detects when the hook is dropped and will unsubscribe when the component is unmounted
root: Option<ProvidedState<T>>, struct UseSharedStateOwner<T> {
value: Option<Rc<RefCell<T>>>, state: UseSharedState<T>,
scope_id: ScopeId, scope_id: ScopeId,
needs_notification: Cell<bool>,
} }
impl<T> Drop for SharedStateInner<T> {
impl<T> Drop for UseSharedStateOwner<T> {
fn drop(&mut self) { fn drop(&mut self) {
// we need to unsubscribe when our component is unounted // we need to unsubscribe when our component is unmounted
if let Some(root) = &self.root { let mut root = self.state.inner.borrow_mut();
let mut root = root.borrow_mut(); root.consumers.remove(&self.scope_id);
root.consumers.remove(&self.scope_id);
}
} }
} }
pub struct UseSharedState<'a, T: 'static> { /// State that is shared between components through the context system
pub(crate) cx: &'a ScopeState, pub struct UseSharedState<T> {
pub(crate) value: &'a Rc<RefCell<T>>, pub(crate) inner: Rc<RefCell<ProvidedStateInner<T>>>,
pub(crate) root: &'a Rc<RefCell<ProvidedStateInner<T>>>,
pub(crate) needs_notification: &'a Cell<bool>,
} }
impl<'a, T: 'static> UseSharedState<'a, T> { 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();
}
/// Read the shared value
pub fn read(&self) -> Ref<'_, T> { pub fn read(&self) -> Ref<'_, T> {
self.value.borrow() Ref::map(self.inner.borrow(), |inner| &inner.value)
}
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)
} }
/// Calling "write" will force the component to re-render /// 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> { pub fn write(&self) -> RefMut<'_, T> {
self.cx.needs_update(); let mut value = self.inner.borrow_mut();
self.notify_consumers(); value.notify_consumers();
self.value.borrow_mut() RefMut::map(value, |inner| &mut inner.value)
} }
/// Allows the ability to write the value without forcing a re-render /// Allows the ability to write the value without forcing a re-render
pub fn write_silent(&self) -> RefMut<'_, T> { pub fn write_silent(&self) -> RefMut<'_, T> {
self.value.borrow_mut() RefMut::map(self.inner.borrow_mut(), |inner| &mut inner.value)
}
pub fn inner(&self) -> Rc<RefCell<ProvidedStateInner<T>>> {
self.root.clone()
} }
} }
impl<T> Copy for UseSharedState<'_, T> {} impl<T> Clone for UseSharedState<T> {
impl<'a, T> Clone for UseSharedState<'a, T>
where
T: 'static,
{
fn clone(&self) -> Self { fn clone(&self) -> Self {
UseSharedState { Self {
cx: self.cx, inner: self.inner.clone(),
value: self.value,
root: self.root,
needs_notification: self.needs_notification,
} }
} }
} }
/// 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) { pub fn use_shared_state_provider<T: 'static>(cx: &ScopeState, f: impl FnOnce() -> T) {
cx.use_hook(|| { cx.use_hook(|| {
let state: ProvidedState<T> = Rc::new(RefCell::new(ProvidedStateInner { let state: ProvidedState<T> = Rc::new(RefCell::new(ProvidedStateInner {
value: Rc::new(RefCell::new(f())), value: f(),
notify_any: cx.schedule_update_any(), notify_any: cx.schedule_update_any(),
consumers: HashSet::new(), consumers: HashSet::new(),
})); }));