use self::error::{UseSharedStateError, UseSharedStateResult}; use dioxus_core::{ScopeId, ScopeState}; use std::{collections::HashSet, rc::Rc, sync::Arc}; #[cfg(debug_assertions)] pub use dioxus_debug_cell::{ error::{BorrowError, BorrowMutError}, Ref, RefCell, RefMut, }; #[cfg(not(debug_assertions))] pub use std::cell::{BorrowError, BorrowMutError, Ref, RefCell, RefMut}; #[macro_export] macro_rules! debug_location { () => {{ #[cfg(debug_assertions)] { std::panic::Location::caller() } #[cfg(not(debug_assertions))] { () } }}; } pub mod error { fn locations_display(locations: &[&'static std::panic::Location<'static>]) -> String { locations .iter() .map(|location| format!(" - {location}")) .collect::>() .join("\n") } #[derive(thiserror::Error, Debug)] pub enum UseSharedStateError { #[cfg_attr( debug_assertions, error( "[{0}] {1} is already borrowed at, so it cannot be borrowed mutably. Previous borrows:\n[{2}]\n\n", .source.attempted_at, .type_name, locations_display(&.source.already_borrowed_at) ) )] #[cfg_attr( not(debug_assertions), error("{type_name} is already borrowed, so it cannot be borrowed mutably. (More detail available in debug mode)") )] AlreadyBorrowed { source: super::BorrowMutError, type_name: &'static str, }, #[cfg_attr( debug_assertions, error( "[{0}] {1} is already borrowed mutably at [{2}], so it cannot be borrowed anymore.", .source.attempted_at, .type_name, locations_display(&.source.already_borrowed_at) ) )] #[cfg_attr( not(debug_assertions), error("{type_name} is already borrowed mutably, so it cannot be borrowed anymore. (More detail available in debug mode)") )] AlreadyBorrowedMutably { source: super::BorrowError, type_name: &'static str, }, } pub type UseSharedStateResult = Result; } type ProvidedState = Rc>>; // Tracks all the subscribers to a shared State pub(crate) struct ProvidedStateInner { value: T, notify_any: Arc, consumers: HashSet, } impl ProvidedStateInner { pub(crate) fn notify_consumers(&mut self) { for consumer in self.consumers.iter() { (self.notify_any)(*consumer); } } } /// 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 current_theme { /// 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 state = UseSharedState::new(root); let owner = UseSharedStateOwner { state, scope_id }; Some(owner) }); state.as_ref().map(|s| &s.state) } /// This wrapper detects when the hook is dropped and will unsubscribe when the component is unmounted 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.inner.borrow_mut(); root.consumers.remove(&self.scope_id); } } /// State that is shared between components through the context system pub struct UseSharedState { pub(crate) inner: Rc>>, } impl UseSharedState { fn new(inner: Rc>>) -> Self { Self { inner } } /// 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(); } /// Try reading the shared state #[cfg_attr(debug_assertions, track_caller)] #[cfg_attr(debug_assertions, inline(never))] pub fn try_read(&self) -> UseSharedStateResult> { match self.inner.try_borrow() { Ok(value) => Ok(Ref::map(value, |inner| &inner.value)), Err(source) => Err(UseSharedStateError::AlreadyBorrowedMutably { source, type_name: std::any::type_name::(), }), } } /// Read the shared value #[cfg_attr(debug_assertions, track_caller)] #[cfg_attr(debug_assertions, inline(never))] pub fn read(&self) -> Ref<'_, T> { match self.try_read() { Ok(value) => value, Err(message) => panic!( "Reading the shared state failed: {}\n({:?})", message, message ), } } /// Try writing the shared state #[cfg_attr(debug_assertions, track_caller)] #[cfg_attr(debug_assertions, inline(never))] pub fn try_write(&self) -> UseSharedStateResult> { match self.inner.try_borrow_mut() { Ok(mut value) => { value.notify_consumers(); Ok(RefMut::map(value, |inner| &mut inner.value)) } Err(source) => Err(UseSharedStateError::AlreadyBorrowed { source, type_name: std::any::type_name::(), }), } } /// 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 #[cfg_attr(debug_assertions, track_caller)] #[cfg_attr(debug_assertions, inline(never))] pub fn write(&self) -> RefMut<'_, T> { match self.try_write() { Ok(value) => value, Err(message) => panic!( "Writing to shared state failed: {}\n({:?})", message, message ), } } /// Tries writing the value without forcing a re-render #[cfg_attr(debug_assertions, track_caller)] #[cfg_attr(debug_assertions, inline(never))] pub fn try_write_silent(&self) -> UseSharedStateResult> { match self.inner.try_borrow_mut() { Ok(value) => Ok(RefMut::map(value, |inner| &mut inner.value)), Err(source) => Err(UseSharedStateError::AlreadyBorrowed { source, type_name: std::any::type_name::(), }), } } /// Writes the value without forcing a re-render #[cfg_attr(debug_assertions, track_caller)] #[cfg_attr(debug_assertions, inline(never))] pub fn write_silent(&self) -> RefMut<'_, T> { match self.try_write_silent() { Ok(value) => value, Err(message) => panic!( "Writing to shared state silently failed: {}\n({:?})", message, message ), } } } impl Clone for UseSharedState { fn clone(&self) -> Self { Self { inner: self.inner.clone(), } } } impl PartialEq for UseSharedState { 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::(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: f(), notify_any: cx.schedule_update_any(), consumers: HashSet::new(), })); cx.provide_context(state); }); }