|
| 1 | +use dioxus_core::prelude::*; |
| 2 | +use std::{ |
| 3 | + cell::{Cell, Ref, RefCell, RefMut}, |
| 4 | + fmt::{Debug, Display}, |
| 5 | + rc::Rc, |
| 6 | +}; |
| 7 | + |
| 8 | +/// Store state between component renders! |
| 9 | +/// |
| 10 | +/// ## Dioxus equivalent of useState, designed for Rust |
| 11 | +/// |
| 12 | +/// The Dioxus version of `useState` for state management inside components. It allows you to ergonomically store and |
| 13 | +/// modify state between component renders. When the state is updated, the component will re-render. |
| 14 | +/// |
| 15 | +/// Dioxus' use_state basically wraps a RefCell with helper methods and integrates it with the VirtualDOM update system. |
| 16 | +/// |
| 17 | +/// [`use_state`] exposes a few helper methods to modify the underlying state: |
| 18 | +/// - `.set(new)` allows you to override the "work in progress" value with a new value |
| 19 | +/// - `.get_mut()` allows you to modify the WIP value |
| 20 | +/// - `.get_wip()` allows you to access the WIP value |
| 21 | +/// - `.deref()` provides the previous value (often done implicitly, though a manual dereference with `*` might be required) |
| 22 | +/// |
| 23 | +/// Additionally, a ton of std::ops traits are implemented for the `UseState` wrapper, meaning any mutative type operations |
| 24 | +/// will automatically be called on the WIP value. |
| 25 | +/// |
| 26 | +/// ## Combinators |
| 27 | +/// |
| 28 | +/// On top of the methods to set/get state, `use_state` also supports fancy combinators to extend its functionality: |
| 29 | +/// - `.classic()` and `.split()` convert the hook into the classic React-style hook |
| 30 | +/// ```rust |
| 31 | +/// let (state, set_state) = use_state(&cx, || 10).split() |
| 32 | +/// ``` |
| 33 | +/// Usage: |
| 34 | +/// |
| 35 | +/// ```ignore |
| 36 | +/// const Example: Component = |cx| { |
| 37 | +/// let counter = use_state(&cx, || 0); |
| 38 | +/// |
| 39 | +/// cx.render(rsx! { |
| 40 | +/// div { |
| 41 | +/// h1 { "Counter: {counter}" } |
| 42 | +/// button { onclick: move |_| counter.set(**counter + 1), "Increment" } |
| 43 | +/// button { onclick: move |_| counter.set(**counter - 1), "Decrement" } |
| 44 | +/// } |
| 45 | +/// )) |
| 46 | +/// } |
| 47 | +/// ``` |
| 48 | +pub fn use_state<'a, T: 'static>( |
| 49 | + cx: &'a ScopeState, |
| 50 | + initial_state_fn: impl FnOnce() -> T, |
| 51 | +) -> &'a UseState<T> { |
| 52 | + let hook = cx.use_hook(move |_| UseState { |
| 53 | + current_val: Rc::new(initial_state_fn()), |
| 54 | + update_callback: cx.schedule_update(), |
| 55 | + wip: Rc::new(RefCell::new(None)), |
| 56 | + update_scheuled: Cell::new(false), |
| 57 | + }); |
| 58 | + |
| 59 | + hook.update_scheuled.set(false); |
| 60 | + let mut new_val = hook.wip.borrow_mut(); |
| 61 | + |
| 62 | + if new_val.is_some() { |
| 63 | + // if there's only one reference (weak or otherwise), we can just swap the values |
| 64 | + if let Some(val) = Rc::get_mut(&mut hook.current_val) { |
| 65 | + *val = new_val.take().unwrap(); |
| 66 | + } else { |
| 67 | + hook.current_val = Rc::new(new_val.take().unwrap()); |
| 68 | + } |
| 69 | + } |
| 70 | + |
| 71 | + hook |
| 72 | +} |
| 73 | + |
| 74 | +pub struct UseState<T: 'static> { |
| 75 | + pub(crate) current_val: Rc<T>, |
| 76 | + pub(crate) wip: Rc<RefCell<Option<T>>>, |
| 77 | + pub(crate) update_callback: Rc<dyn Fn()>, |
| 78 | + pub(crate) update_scheuled: Cell<bool>, |
| 79 | +} |
| 80 | + |
| 81 | +impl<T: Debug> Debug for UseState<T> { |
| 82 | + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { |
| 83 | + write!(f, "{:?}", self.current_val) |
| 84 | + } |
| 85 | +} |
| 86 | + |
| 87 | +impl<T: 'static> UseState<T> { |
| 88 | + /// Tell the Dioxus Scheduler that we need to be processed |
| 89 | + pub fn needs_update(&self) { |
| 90 | + if !self.update_scheuled.get() { |
| 91 | + self.update_scheuled.set(true); |
| 92 | + (self.update_callback)(); |
| 93 | + } |
| 94 | + } |
| 95 | + |
| 96 | + pub fn set(&self, new_val: T) { |
| 97 | + *self.wip.borrow_mut() = Some(new_val); |
| 98 | + self.needs_update(); |
| 99 | + } |
| 100 | + |
| 101 | + pub fn get(&self) -> &T { |
| 102 | + &self.current_val |
| 103 | + } |
| 104 | + |
| 105 | + pub fn get_rc(&self) -> &Rc<T> { |
| 106 | + &self.current_val |
| 107 | + } |
| 108 | + |
| 109 | + /// Get the current status of the work-in-progress data |
| 110 | + pub fn get_wip(&self) -> Ref<Option<T>> { |
| 111 | + self.wip.borrow() |
| 112 | + } |
| 113 | + |
| 114 | + /// Get the current status of the work-in-progress data |
| 115 | + pub fn get_wip_mut(&self) -> RefMut<Option<T>> { |
| 116 | + self.wip.borrow_mut() |
| 117 | + } |
| 118 | + |
| 119 | + pub fn split(&self) -> (&T, Rc<dyn Fn(T)>) { |
| 120 | + (&self.current_val, self.setter()) |
| 121 | + } |
| 122 | + |
| 123 | + pub fn setter(&self) -> Rc<dyn Fn(T)> { |
| 124 | + let slot = self.wip.clone(); |
| 125 | + let callback = self.update_callback.clone(); |
| 126 | + Rc::new(move |new| { |
| 127 | + callback(); |
| 128 | + *slot.borrow_mut() = Some(new) |
| 129 | + }) |
| 130 | + } |
| 131 | + |
| 132 | + pub fn wtih(&self, f: impl FnOnce(&mut T)) { |
| 133 | + let mut val = self.wip.borrow_mut(); |
| 134 | + |
| 135 | + if let Some(inner) = val.as_mut() { |
| 136 | + f(inner); |
| 137 | + } |
| 138 | + } |
| 139 | + |
| 140 | + pub fn for_async(&self) -> UseState<T> { |
| 141 | + let UseState { |
| 142 | + current_val, |
| 143 | + wip, |
| 144 | + update_callback, |
| 145 | + update_scheuled, |
| 146 | + } = self; |
| 147 | + |
| 148 | + UseState { |
| 149 | + current_val: current_val.clone(), |
| 150 | + wip: wip.clone(), |
| 151 | + update_callback: update_callback.clone(), |
| 152 | + update_scheuled: update_scheuled.clone(), |
| 153 | + } |
| 154 | + } |
| 155 | +} |
| 156 | + |
| 157 | +impl<T: 'static + ToOwned<Owned = T>> UseState<T> { |
| 158 | + /// Gain mutable access to the new value via [`RefMut`]. |
| 159 | + /// |
| 160 | + /// If `modify` is called, then the component will re-render. |
| 161 | + /// |
| 162 | + /// This method is only available when the value is a `ToOwned` type. |
| 163 | + /// |
| 164 | + /// Mutable access is derived by calling "ToOwned" (IE cloning) on the current value. |
| 165 | + /// |
| 166 | + /// To get a reference to the current value, use `.get()` |
| 167 | + pub fn modify(&self) -> RefMut<T> { |
| 168 | + // make sure we get processed |
| 169 | + self.needs_update(); |
| 170 | + |
| 171 | + // Bring out the new value, cloning if it we need to |
| 172 | + // "get_mut" is locked behind "ToOwned" to make it explicit that cloning occurs to use this |
| 173 | + RefMut::map(self.wip.borrow_mut(), |slot| { |
| 174 | + if slot.is_none() { |
| 175 | + *slot = Some(self.current_val.as_ref().to_owned()); |
| 176 | + } |
| 177 | + slot.as_mut().unwrap() |
| 178 | + }) |
| 179 | + } |
| 180 | + |
| 181 | + pub fn inner(self) -> T { |
| 182 | + self.current_val.as_ref().to_owned() |
| 183 | + } |
| 184 | +} |
| 185 | + |
| 186 | +impl<'a, T> std::ops::Deref for UseState<T> { |
| 187 | + type Target = T; |
| 188 | + |
| 189 | + fn deref(&self) -> &Self::Target { |
| 190 | + self.get() |
| 191 | + } |
| 192 | +} |
| 193 | + |
| 194 | +// enable displaty for the handle |
| 195 | +impl<'a, T: 'static + Display> std::fmt::Display for UseState<T> { |
| 196 | + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { |
| 197 | + write!(f, "{}", self.current_val) |
| 198 | + } |
| 199 | +} |
| 200 | + |
| 201 | +impl<'a, V, T: PartialEq<V>> PartialEq<V> for UseState<T> { |
| 202 | + fn eq(&self, other: &V) -> bool { |
| 203 | + self.get() == other |
| 204 | + } |
| 205 | +} |
| 206 | +impl<'a, O, T: std::ops::Not<Output = O> + Copy> std::ops::Not for UseState<T> { |
| 207 | + type Output = O; |
| 208 | + |
| 209 | + fn not(self) -> Self::Output { |
| 210 | + !*self.get() |
| 211 | + } |
| 212 | +} |
0 commit comments