1
0

use_shared_state.rs 8.6 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297
  1. use dioxus_core::{ScopeId, ScopeState};
  2. use std::{
  3. cell::{Ref, RefCell, RefMut},
  4. collections::HashSet,
  5. panic::Location,
  6. rc::Rc,
  7. sync::Arc,
  8. };
  9. type ProvidedState<T> = Rc<RefCell<ProvidedStateInner<T>>>;
  10. // Tracks all the subscribers to a shared State
  11. pub(crate) struct ProvidedStateInner<T> {
  12. value: T,
  13. notify_any: Arc<dyn Fn(ScopeId)>,
  14. consumers: HashSet<ScopeId>,
  15. }
  16. impl<T> ProvidedStateInner<T> {
  17. pub(crate) fn notify_consumers(&mut self) {
  18. for consumer in self.consumers.iter() {
  19. (self.notify_any)(*consumer);
  20. }
  21. }
  22. }
  23. /// This hook provides some relatively light ergonomics around shared state.
  24. ///
  25. /// It is not a substitute for a proper state management system, but it is capable enough to provide use_state - type
  26. /// ergonomics in a pinch, with zero cost.
  27. ///
  28. /// # Example
  29. ///
  30. /// ```rust
  31. /// # use dioxus::prelude::*;
  32. /// #
  33. /// # fn app(cx: Scope) -> Element {
  34. /// # render! {
  35. /// # Parent{}
  36. /// # }
  37. /// # }
  38. ///
  39. /// #[derive(Clone, Copy)]
  40. /// enum Theme {
  41. /// Light,
  42. /// Dark,
  43. /// }
  44. ///
  45. /// // Provider
  46. /// fn Parent<'a>(cx: Scope<'a>) -> Element<'a> {
  47. /// use_shared_state_provider(cx, || Theme::Dark);
  48. /// let theme = use_shared_state::<Theme>(cx).unwrap();
  49. ///
  50. /// render! {
  51. /// button{
  52. /// onclick: move |_| {
  53. /// let current_theme = *theme.read();
  54. /// *theme.write() = match current_theme {
  55. /// Theme::Dark => Theme::Light,
  56. /// Theme::Light => Theme::Dark,
  57. /// };
  58. /// },
  59. /// "Change theme"
  60. /// }
  61. /// Child{}
  62. /// }
  63. /// }
  64. ///
  65. /// // Consumer
  66. /// fn Child<'a>(cx: Scope<'a>) -> Element<'a> {
  67. /// let theme = use_shared_state::<Theme>(cx).unwrap();
  68. /// let current_theme = *theme.read();
  69. ///
  70. /// render! {
  71. /// match current_theme {
  72. /// Theme::Dark => {
  73. /// "Dark mode"
  74. /// }
  75. /// Theme::Light => {
  76. /// "Light mode"
  77. /// }
  78. /// }
  79. /// }
  80. /// }
  81. /// ```
  82. ///
  83. /// # How it works
  84. ///
  85. /// Any time a component calls `write`, every consumer of the state will be notified - excluding the provider.
  86. ///
  87. /// Right now, there is not a distinction between read-only and write-only, so every consumer will be notified.
  88. pub fn use_shared_state<T: 'static>(cx: &ScopeState) -> Option<&UseSharedState<T>> {
  89. let state: &Option<UseSharedStateOwner<T>> = &*cx.use_hook(move || {
  90. let scope_id = cx.scope_id();
  91. let root = cx.consume_context::<ProvidedState<T>>()?;
  92. root.borrow_mut().consumers.insert(scope_id);
  93. let state = UseSharedState { inner: root };
  94. let owner = UseSharedStateOwner { state, scope_id };
  95. Some(owner)
  96. });
  97. state.as_ref().map(|s| &s.state)
  98. }
  99. /// This wrapper detects when the hook is dropped and will unsubscribe when the component is unmounted
  100. struct UseSharedStateOwner<T> {
  101. state: UseSharedState<T>,
  102. scope_id: ScopeId,
  103. }
  104. impl<T> Drop for UseSharedStateOwner<T> {
  105. fn drop(&mut self) {
  106. // we need to unsubscribe when our component is unmounted
  107. let mut root = self.state.inner.borrow_mut();
  108. root.consumers.remove(&self.scope_id);
  109. }
  110. }
  111. /// State that is shared between components through the context system
  112. pub struct UseSharedState<T> {
  113. pub(crate) inner: Rc<RefCell<ProvidedStateInner<T>>>,
  114. }
  115. #[derive(thiserror::Error, Debug)]
  116. pub enum UseSharedStateError {
  117. #[error("[{caller}] {type_name} is already borrowed, so it cannot be borrowed mutably.")]
  118. AlreadyBorrowed {
  119. source: core::cell::BorrowMutError,
  120. type_name: &'static str,
  121. caller: &'static Location<'static>,
  122. },
  123. #[error("[caller] {type_name} is already borrowed mutably, so it cannot be borrowed anymore.")]
  124. AlreadyBorrowedMutably {
  125. source: core::cell::BorrowError,
  126. type_name: &'static str,
  127. caller: &'static Location<'static>,
  128. },
  129. }
  130. pub type UseSharedStateResult<T> = Result<T, UseSharedStateError>;
  131. impl<T> UseSharedState<T> {
  132. /// Notify all consumers of the state that it has changed. (This is called automatically when you call "write")
  133. pub fn notify_consumers(&self) {
  134. self.inner.borrow_mut().notify_consumers();
  135. }
  136. /// Try reading the shared state
  137. #[track_caller]
  138. pub fn try_read(&self) -> UseSharedStateResult<Ref<'_, T>> {
  139. self.inner
  140. .try_borrow()
  141. .map_err(|source| UseSharedStateError::AlreadyBorrowedMutably {
  142. source,
  143. type_name: std::any::type_name::<Self>(),
  144. caller: Location::caller(),
  145. })
  146. .map(|value| Ref::map(value, |inner| &inner.value))
  147. }
  148. /// Read the shared value
  149. #[track_caller]
  150. pub fn read(&self) -> Ref<'_, T> {
  151. match self.try_read() {
  152. Ok(value) => value,
  153. Err(message) => panic!(
  154. "Reading the shared state failed: {}\n({:?})",
  155. message, message
  156. ),
  157. }
  158. }
  159. /// Try writing the shared state
  160. #[track_caller]
  161. pub fn try_write(&self) -> UseSharedStateResult<RefMut<'_, T>> {
  162. self.inner
  163. .try_borrow_mut()
  164. .map_err(|source| UseSharedStateError::AlreadyBorrowed {
  165. source,
  166. type_name: std::any::type_name::<Self>(),
  167. caller: Location::caller(),
  168. })
  169. .map(|mut value| {
  170. value.notify_consumers();
  171. RefMut::map(value, |inner| &mut inner.value)
  172. })
  173. }
  174. /// Calling "write" will force the component to re-render
  175. ///
  176. ///
  177. // TODO: We prevent unncessary notifications only in the hook, but we should figure out some more global lock
  178. #[track_caller]
  179. pub fn write(&self) -> RefMut<'_, T> {
  180. match self.try_write() {
  181. Ok(value) => value,
  182. Err(message) => panic!(
  183. "Writing to shared state failed: {}\n({:?})",
  184. message, message
  185. ),
  186. }
  187. }
  188. /// Tries writing the value without forcing a re-render
  189. #[track_caller]
  190. pub fn try_write_silent(&self) -> UseSharedStateResult<RefMut<'_, T>> {
  191. self.inner
  192. .try_borrow_mut()
  193. .map_err(|source| UseSharedStateError::AlreadyBorrowed {
  194. source,
  195. type_name: std::any::type_name::<Self>(),
  196. caller: Location::caller(),
  197. })
  198. .map(|value| RefMut::map(value, |inner| &mut inner.value))
  199. }
  200. /// Writes the value without forcing a re-render
  201. #[track_caller]
  202. pub fn write_silent(&self) -> RefMut<'_, T> {
  203. match self.try_write_silent() {
  204. Ok(value) => value,
  205. Err(message) => panic!(
  206. "Writing to shared state silently failed: {}\n({:?})",
  207. message, message
  208. ),
  209. }
  210. }
  211. }
  212. impl<T> Clone for UseSharedState<T> {
  213. fn clone(&self) -> Self {
  214. Self {
  215. inner: self.inner.clone(),
  216. }
  217. }
  218. }
  219. impl<T: PartialEq> PartialEq for UseSharedState<T> {
  220. fn eq(&self, other: &Self) -> bool {
  221. let first = self.inner.borrow();
  222. let second = other.inner.borrow();
  223. first.value == second.value
  224. }
  225. }
  226. /// Provide some state for components down the hierarchy to consume without having to drill props. See [`use_shared_state`] to consume the state
  227. ///
  228. ///
  229. /// # Example
  230. ///
  231. /// ```rust
  232. /// # use dioxus::prelude::*;
  233. /// #
  234. /// # fn app(cx: Scope) -> Element {
  235. /// # render! {
  236. /// # Parent{}
  237. /// # }
  238. /// # }
  239. ///
  240. /// #[derive(Clone, Copy)]
  241. /// enum Theme {
  242. /// Light,
  243. /// Dark,
  244. /// }
  245. ///
  246. /// // Provider
  247. /// fn Parent<'a>(cx: Scope<'a>) -> Element<'a> {
  248. /// use_shared_state_provider(cx, || Theme::Dark);
  249. /// let theme = use_shared_state::<Theme>(cx).unwrap();
  250. ///
  251. /// render! {
  252. /// button{
  253. /// onclick: move |_| {
  254. /// let current_theme = *theme.read();
  255. /// *theme.write() = match current_theme {
  256. /// Theme::Dark => Theme::Light,
  257. /// Theme::Light => Theme::Dark,
  258. /// };
  259. /// },
  260. /// "Change theme"
  261. /// }
  262. /// // Children components that consume the state...
  263. /// }
  264. /// }
  265. /// ```
  266. pub fn use_shared_state_provider<T: 'static>(cx: &ScopeState, f: impl FnOnce() -> T) {
  267. cx.use_hook(|| {
  268. let state: ProvidedState<T> = Rc::new(RefCell::new(ProvidedStateInner {
  269. value: f(),
  270. notify_any: cx.schedule_update_any(),
  271. consumers: HashSet::new(),
  272. }));
  273. cx.provide_context(state);
  274. });
  275. }