wip: some project refactor

This commit is contained in:
Jonathan Kelley committed 2021-07-09 11:54:07 -04:00
1 parent feab50f24a
commit 8cfc437bfe
27 files changed
+430 -677

No files matched your search

+2 -115
View File
@@ -2,118 +2,5 @@ use std::collections::HashMap;
use dioxus_core::prelude::*;
/*
a form of use_state explicitly for map-style collections (BTreeMap, HashMap, etc).
Why?
---
Traditionally, it's possible to use the "use_state" hook for collections in the React world.
Adding a new entry would look something similar to:
```js
let (map, set_map) = useState({});
set_map({ ...map, [key]: value });
```
The new value then causes the appropriate update when passed into children.
This is moderately efficient because the fields of the map are moved, but the data itself is not cloned.
However, if you used similar approach with Dioxus:
```rust
let (map, set_map) = use_state(cx, || HashMap::new());
set_map({
let mut newmap = map.clone();
newmap.set(key, value);
newmap
})
```
Unfortunately, you'd be cloning the entire state every time a value is changed. The obvious solution is to
wrap every element in the HashMap with an Rc. That way, cloning the HashMap is on par with its JS equivalent.
Fortunately, we can make this operation even more efficient in Dioxus, leveraging the borrow rules of Rust.
This hook provides a memoized collection, memoized setters, and memoized getters. This particular hook is
extremely powerful for implementing lists and supporting core state management needs for small apps.
If you need something even more powerful, check out the dedicated atomic state management Dioxus Dataflow, which
uses the same memoization on top of the use_context API.
Here's a fully-functional todo app using the use_map API:
```rust
static TodoList: FC<()> = |cx| {
let todos = use_map(cx, || HashMap::new());
let input = use_ref(|| None);
cx.render(rsx!{
div {
button {
"Add todo"
onclick: move |_| {
let new_todo = TodoItem::new(input.contents());
todos.insert(new_todo.id.clone(), new_todo);
input.clear();
}
}
button {
"Clear todos"
onclick: move |_| todos.clear()
}
input {
placeholder: "What needs to be done?"
ref: input
}
ul {
{todos.iter().map(|todo| rsx!(
li {
key: todo.id
span { "{todo.content}" }
button {"x", onclick: move |_| todos.remove(todo.key.clone())}
}
))}
}
}
})
}
```
*/
fn use_map() {}
// a form of "use_state" that allows collection memoization
// Elements are received as Rc<T> in case the underlying collection is shuffled around
// Setters/getters can be generated
fn use_collection<'a, T: Collection>(
cx: &impl Scoped<'a>,
f: impl Fn() -> T,
) -> CollectionHandle<'a, T> {
cx.use_hook(
|| {},
|h| {
//
CollectionHandle {
_p: Default::default(),
}
},
|h| {},
)
}
struct CollectionMemo {}
struct CollectionHandle<'a, T: Collection> {
_p: std::marker::PhantomData<&'a T>,
}
trait Collection {}
impl<K, V> Collection for std::collections::HashMap<K, V> {}
struct MapCollection<K, V> {
inner: HashMap<K, V>,
}
impl<K, V> MapCollection<K, V> {
fn set(&self, key: K, val: V) {
//
}
}
mod usestate;
pub use usestate::{use_state, UseState};
+76
View File
@@ -0,0 +1,76 @@
/*
a form of use_state explicitly for map-style collections (BTreeMap, HashMap, etc).
Why?
---
Traditionally, it's possible to use the "use_state" hook for collections in the React world.
Adding a new entry would look something similar to:
```js
let (map, set_map) = useState({});
set_map({ ...map, [key]: value });
```
The new value then causes the appropriate update when passed into children.
This is moderately efficient because the fields of the map are moved, but the data itself is not cloned.
However, if you used similar approach with Dioxus:
```rust
let (map, set_map) = use_state(cx, || HashMap::new());
set_map({
let mut newmap = map.clone();
newmap.set(key, value);
newmap
})
```
Unfortunately, you'd be cloning the entire state every time a value is changed. The obvious solution is to
wrap every element in the HashMap with an Rc. That way, cloning the HashMap is on par with its JS equivalent.
Fortunately, we can make this operation even more efficient in Dioxus, leveraging the borrow rules of Rust.
This hook provides a memoized collection, memoized setters, and memoized getters. This particular hook is
extremely powerful for implementing lists and supporting core state management needs for small apps.
If you need something even more powerful, check out the dedicated atomic state management Dioxus Dataflow, which
uses the same memoization on top of the use_context API.
Here's a fully-functional todo app using the use_map API:
```rust
static TodoList: FC<()> = |cx| {
let todos = use_map(cx, || HashMap::new());
let input = use_ref(|| None);
cx.render(rsx!{
div {
button {
"Add todo"
onclick: move |_| {
let new_todo = TodoItem::new(input.contents());
todos.insert(new_todo.id.clone(), new_todo);
input.clear();
}
}
button {
"Clear todos"
onclick: move |_| todos.clear()
}
input {
placeholder: "What needs to be done?"
ref: input
}
ul {
{todos.iter().map(|todo| rsx!(
li {
key: todo.id
span { "{todo.content}" }
button {"x", onclick: move |_| todos.remove(todo.key.clone())}
}
))}
}
}
})
}
```
*/
View File
Whitespace-only changes.
View File
Whitespace-only changes.
+170
View File
@@ -0,0 +1,170 @@
use dioxus_core::prelude::Context;
use std::{
cell::{Cell, Ref, RefCell, RefMut},
fmt::Display,
ops::{Deref, DerefMut},
rc::Rc,
};
/// Store state between component renders!
///
/// ## The "King" of state hooks
///
/// The Dioxus version of `useState` is the "king daddy" of state management. It allows you to ergonomically store and
/// modify state between component renders. When the state is updated, the component will re-render.
///
/// Dioxus' use_state basically wraps a RefCell with helper methods and integrates it with the VirtualDOM update system.
///
/// [`use_state`] exposes a few helper methods to modify the underlying state:
/// - `.set(new)` allows you to override the "work in progress" value with a new value
/// - `.get_mut()` allows you to modify the WIP value
/// - `.get_wip()` allows you to access the WIP value
/// - `.deref()` provides the previous value (often done implicitly, though a manual dereference with `*` might be required)
///
/// Additionally, a ton of std::ops traits are implemented for the `UseState` wrapper, meaning any mutative type operations
/// will automatically be called on the WIP value.
///
///
/// Usage:
/// ```ignore
/// const Example: FC<()> = |cx| {
/// let counter = use_state(cx, || 0);
/// let increment = |_| counter += 1;
/// let decrement = |_| counter += 1;
///
/// html! {
/// <div>
/// <h1>"Counter: {counter}" </h1>
/// <button onclick={increment}> "Increment" </button>
/// <button onclick={decrement}> "Decrement" </button>
/// </div>
/// }
/// }
/// ```
pub fn use_state<'a, 'c, T: 'static, F: FnOnce() -> T, P>(
cx: Context<'a, P>,
initial_state_fn: F,
) -> UseState<T> {
cx.use_hook(
move || UseStateInner {
current_val: initial_state_fn(),
callback: cx.schedule_update(),
wip: RefCell::new(None),
update_scheuled: Cell::new(false),
},
move |hook| {
hook.update_scheuled.set(false);
let mut new_val = hook.wip.borrow_mut();
if new_val.is_some() {
hook.current_val = new_val.take().unwrap();
}
UseState { inner: &*hook }
},
|_| {},
)
}
struct UseStateInner<T: 'static> {
current_val: T,
update_scheuled: Cell<bool>,
callback: Rc<dyn Fn()>,
wip: RefCell<Option<T>>,
updater:
}
pub struct UseState<'a, T: 'static> {
inner: &'a UseStateInner<T>,
}
impl<T> Copy for UseState<'_, T> {}
impl<'a, T> Clone for UseState<'a, T>
where
T: 'static,
{
fn clone(&self) -> Self {
UseState { inner: self.inner }
}
}
impl<'a, T: 'static> UseState<'a, T> {
/// Tell the Dioxus Scheduler that we need to be processed
pub fn needs_update(&self) {
if !self.inner.update_scheuled.get() {
self.inner.update_scheuled.set(true);
(self.inner.callback)();
}
}
pub fn set(&self, new_val: T) {
self.needs_update();
*self.inner.wip.borrow_mut() = Some(new_val);
}
pub fn get(&self) -> &T {
&self.inner.current_val
}
/// Get the current status of the work-in-progress data
pub fn get_wip(&self) -> Ref<Option<T>> {
self.inner.wip.borrow()
}
pub fn classic(self) -> (&'a T, &'a Rc<dyn Fn(T)>) {
(&self.inner.current_val)
}
}
impl<'a, T: 'static + ToOwned<Owned = T>> UseState<'a, T> {
pub fn get_mut(self) -> RefMut<'a, T> {
// make sure we get processed
self.needs_update();
// Bring out the new value, cloning if it we need to
// "get_mut" is locked behind "ToOwned" to make it explicit that cloning occurs to use this
RefMut::map(self.inner.wip.borrow_mut(), |slot| {
if slot.is_none() {
*slot = Some(self.inner.current_val.to_owned());
}
slot.as_mut().unwrap()
})
}
}
impl<'a, T: 'static> std::ops::Deref for UseState<'a, T> {
type Target = T;
fn deref(&self) -> &Self::Target {
&self.inner.current_val
}
}
use std::ops::{Add, AddAssign, Sub, SubAssign};
impl<'a, T: Copy + Add<T, Output = T>> Add<T> for UseState<'a, T> {
type Output = T;
fn add(self, rhs: T) -> Self::Output {
self.inner.current_val.add(rhs)
}
}
impl<'a, T: Copy + Add<T, Output = T>> AddAssign<T> for UseState<'a, T> {
fn add_assign(&mut self, rhs: T) {
self.set(self.inner.current_val.add(rhs));
}
}
impl<'a, T: Copy + Sub<T, Output = T>> Sub<T> for UseState<'a, T> {
type Output = T;
fn sub(self, rhs: T) -> Self::Output {
self.inner.current_val.sub(rhs)
}
}
impl<'a, T: Copy + Sub<T, Output = T>> SubAssign<T> for UseState<'a, T> {
fn sub_assign(&mut self, rhs: T) {
self.set(self.inner.current_val.sub(rhs));
}
}
// enable displaty for the handle
impl<'a, T: 'static + Display> std::fmt::Display for UseState<'a, T> {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(f, "{}", self.inner.current_val)
}
}
View File
Whitespace-only changes.