use dioxus_core::{ScopeId, ScopeState}; use std::{ cell::{Ref, RefCell, RefMut}, collections::HashSet, rc::Rc, sync::Arc, }; type ProvidedState = Rc>>; // Tracks all the subscribers to a shared State pub struct ProvidedStateInner { value: Rc>, notify_any: Arc, consumers: HashSet, } impl ProvidedStateInner { pub(crate) fn notify_consumers(&mut self) { for consumer in self.consumers.iter() { (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. /// /// It is not a substitute for a proper state management system, but it is capable enough to provide use_state - type /// ergonomics in a pinch, with zero cost. /// /// # 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" /// } /// 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 /// /// 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<&UseSharedState> { let state: &Option> = &*cx.use_hook(move || { let scope_id = cx.scope_id(); let root = cx.consume_context::>()?; root.borrow_mut().consumers.insert(scope_id); let value = root.borrow().value.clone(); let state = UseSharedState { value, root }; let owner = UseSharedStateOwner { state, scope_id }; Some(owner) }); state.as_ref().map(|s| &s.state) } struct UseSharedStateOwner { state: UseSharedState, scope_id: ScopeId, } 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(); root.consumers.remove(&self.scope_id); } } pub struct UseSharedState { pub(crate) value: Rc>, pub(crate) root: Rc>>, } impl UseSharedState { pub fn read(&self) -> Ref<'_, T> { self.value.borrow() } pub fn notify_consumers(&self) { self.root.borrow_mut().notify_consumers(); } pub fn read_write(&self) -> (Ref<'_, T>, &Self) { (self.read(), self) } /// 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 pub fn write(&self) -> RefMut<'_, T> { self.notify_consumers(); self.value.borrow_mut() } /// 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() } } impl Clone for UseSharedState { fn clone(&self) -> Self { Self { value: self.value.clone(), root: self.root.clone(), } } } 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 { value: Rc::new(RefCell::new(f())), notify_any: cx.schedule_update_any(), consumers: HashSet::new(), })); cx.provide_context(state); }); }