Skip to content

Commit 58839f4

Browse files
committed
update: modify usestate to be borrowed
1 parent 8e84223 commit 58839f4

File tree

10 files changed

+237
-419
lines changed

10 files changed

+237
-419
lines changed

examples/calculator.rs

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,7 @@ fn main() {
1818
}
1919

2020
fn app(cx: Scope) -> Element {
21-
let display_value: UseState<String> = use_state(&cx, || String::from("0"));
21+
let display_value = use_state(&cx, || String::from("0"));
2222

2323
let input_digit = move |num: u8| {
2424
if display_value.get() == "0" {
@@ -66,11 +66,11 @@ fn app(cx: Scope) -> Element {
6666
class: "calculator-key key-clear",
6767
onclick: move |_| {
6868
display_value.set(String::new());
69-
if display_value != "" {
69+
if *display_value != "" {
7070
display_value.set("0".into());
7171
}
7272
},
73-
[if display_value == "" { "C" } else { "AC" }]
73+
[if *display_value == "" { "C" } else { "AC" }]
7474
}
7575
button {
7676
class: "calculator-key key-sign",

examples/crm.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -38,7 +38,7 @@ fn app(cx: Scope) -> Element {
3838

3939
h1 {"Dioxus CRM Example"}
4040

41-
match *scene {
41+
match scene.get() {
4242
Scene::ClientsList => rsx!(
4343
div { class: "crm",
4444
h2 { margin_bottom: "10px", "List of clients" }

examples/disabled.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@ fn app(cx: Scope) -> Element {
1111
div {
1212
button {
1313
onclick: move |_| disabled.set(!disabled.get()),
14-
"click to " [if *disabled {"enable"} else {"disable"} ] " the lower button"
14+
"click to " [if **disabled {"enable"} else {"disable"} ] " the lower button"
1515
}
1616

1717
button {

examples/dog_app.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -42,7 +42,7 @@ fn app(cx: Scope) -> Element {
4242
))
4343
}
4444
div { flex: "50%",
45-
match &*selected_breed {
45+
match selected_breed.get() {
4646
Some(breed) => rsx!( Breed { breed: breed.clone() } ),
4747
None => rsx!("No Breed selected"),
4848
}

examples/readme.rs

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -9,13 +9,13 @@ fn main() {
99
}
1010

1111
fn app(cx: Scope) -> Element {
12-
let mut count = use_state(&cx, || 0);
12+
let count = use_state(&cx, || 0);
1313

1414
cx.render(rsx! {
1515
div {
1616
h1 { "High-Five counter: {count}" }
17-
button { onclick: move |_| count += 1, "Up high!" }
18-
button { onclick: move |_| count -= 1, "Down low!" }
17+
button { onclick: move |_| *count.modify() += 1, "Up high!" }
18+
button { onclick: move |_| *count.modify() -= 1, "Down low!" }
1919
}
2020
})
2121
}

examples/todomvc.rs

Lines changed: 16 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -19,15 +19,15 @@ pub struct TodoItem {
1919
}
2020

2121
pub fn app(cx: Scope<()>) -> Element {
22-
let todos = use_state(&cx, || im_rc::HashMap::<u32, TodoItem>::default());
22+
let todos = use_state(&cx, im_rc::HashMap::<u32, TodoItem>::default);
2323
let filter = use_state(&cx, || FilterState::All);
2424
let draft = use_state(&cx, || "".to_string());
25-
let mut todo_id = use_state(&cx, || 0);
25+
let (todo_id, set_todo_id) = use_state(&cx, || 0).split();
2626

2727
// Filter the todos based on the filter state
2828
let mut filtered_todos = todos
2929
.iter()
30-
.filter(|(_, item)| match *filter {
30+
.filter(|(_, item)| match **filter {
3131
FilterState::All => true,
3232
FilterState::Active => !item.checked,
3333
FilterState::Completed => item.checked,
@@ -56,19 +56,17 @@ pub fn app(cx: Scope<()>) -> Element {
5656
autofocus: "true",
5757
oninput: move |evt| draft.set(evt.value.clone()),
5858
onkeydown: move |evt| {
59-
if evt.key == "Enter" {
60-
if !draft.is_empty() {
61-
todos.modify().insert(
62-
*todo_id,
63-
TodoItem {
64-
id: *todo_id,
65-
checked: false,
66-
contents: draft.get().clone(),
67-
},
68-
);
69-
todo_id += 1;
70-
draft.set("".to_string());
71-
}
59+
if evt.key == "Enter" && !draft.is_empty() {
60+
todos.modify().insert(
61+
*todo_id,
62+
TodoItem {
63+
id: *todo_id,
64+
checked: false,
65+
contents: draft.get().clone(),
66+
},
67+
);
68+
set_todo_id(todo_id + 1);
69+
draft.set("".to_string());
7270
}
7371
}
7472
}
@@ -90,7 +88,7 @@ pub fn app(cx: Scope<()>) -> Element {
9088
(show_clear_completed).then(|| rsx!(
9189
button {
9290
class: "clear-completed",
93-
onclick: move |_| todos.modify().retain(|_, todo| todo.checked == false),
91+
onclick: move |_| todos.modify().retain(|_, todo| !todo.checked),
9492
"Clear completed"
9593
}
9694
))
@@ -108,7 +106,7 @@ pub fn app(cx: Scope<()>) -> Element {
108106

109107
#[derive(Props)]
110108
pub struct TodoEntryProps<'a> {
111-
todos: UseState<'a, im_rc::HashMap<u32, TodoItem>>,
109+
todos: &'a UseState<im_rc::HashMap<u32, TodoItem>>,
112110
id: u32,
113111
}
114112

packages/hooks/src/usestate.rs

Lines changed: 212 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,212 @@
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

Comments
 (0)