123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375 |
- 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::<Vec<_>>()
- .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<T> = Result<T, UseSharedStateError>;
- }
- type ProvidedState<T> = Rc<RefCell<ProvidedStateInner<T>>>;
- // Tracks all the subscribers to a shared State
- pub(crate) struct ProvidedStateInner<T> {
- value: T,
- notify_any: Arc<dyn Fn(ScopeId)>,
- consumers: HashSet<ScopeId>,
- gen: usize,
- }
- impl<T> ProvidedStateInner<T> {
- pub(crate) fn notify_consumers(&mut self) {
- self.gen += 1;
- 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::<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 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.
- #[must_use]
- pub fn use_shared_state<T: 'static>(cx: &ScopeState) -> Option<&UseSharedState<T>> {
- let state_owner: &mut Option<UseSharedStateOwner<T>> = &mut *cx.use_hook(move || {
- let scope_id = cx.scope_id();
- let root = cx.consume_context::<ProvidedState<T>>()?;
- root.borrow_mut().consumers.insert(scope_id);
- let state = UseSharedState::new(root);
- let owner = UseSharedStateOwner { state, scope_id };
- Some(owner)
- });
- state_owner.as_mut().map(|s| {
- s.state.gen = s.state.inner.borrow().gen;
- &s.state
- })
- }
- /// 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,
- }
- impl<T> Drop for UseSharedStateOwner<T> {
- 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<T> {
- pub(crate) inner: Rc<RefCell<ProvidedStateInner<T>>>,
- gen: usize,
- }
- impl<T> UseSharedState<T> {
- fn new(inner: Rc<RefCell<ProvidedStateInner<T>>>) -> Self {
- let gen = inner.borrow().gen;
- Self { inner, gen }
- }
- /// 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<Ref<'_, T>> {
- 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::<Self>(),
- }),
- }
- }
- /// 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<RefMut<'_, T>> {
- 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::<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
- #[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<RefMut<'_, T>> {
- 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::<Self>(),
- }),
- }
- }
- /// 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
- ),
- }
- }
- /// Take a reference to the inner value temporarily and produce a new value
- #[cfg_attr(debug_assertions, track_caller)]
- #[cfg_attr(debug_assertions, inline(never))]
- pub fn with<O>(&self, immutable_callback: impl FnOnce(&T) -> O) -> O {
- immutable_callback(&*self.read())
- }
- /// Take a mutable reference to the inner value temporarily and produce a new value
- #[cfg_attr(debug_assertions, track_caller)]
- #[cfg_attr(debug_assertions, inline(never))]
- pub fn with_mut<O>(&self, mutable_callback: impl FnOnce(&mut T) -> O) -> O {
- mutable_callback(&mut *self.write())
- }
- }
- impl<T> Clone for UseSharedState<T> {
- fn clone(&self) -> Self {
- Self {
- inner: self.inner.clone(),
- gen: self.gen,
- }
- }
- }
- impl<T> PartialEq for UseSharedState<T> {
- fn eq(&self, other: &Self) -> bool {
- self.gen == other.gen
- }
- }
- /// 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: f(),
- notify_any: cx.schedule_update_any(),
- consumers: HashSet::new(),
- gen: 0,
- }));
- cx.provide_context(state);
- });
- }
|