feat: shared state mechanisms

This commit is contained in:
Jonathan Kelley committed 2021-10-11 15:35:20 -04:00
1 parent da4423c141
commit 4a4c7afca7
12 files changed
+279 -120

No files matched your search

+21 -53
View File
@@ -102,6 +102,10 @@ impl<'src> Context<'src> {
self.scope.memoized_updater.clone() self.scope.memoized_updater.clone()
} }
pub fn needs_update(&self) {
(self.scope.memoized_updater)()
}
/// Schedule an update for any component given its ScopeId. /// Schedule an update for any component given its ScopeId.
/// ///
/// A component's ScopeId can be obtained from `use_hook` or the [`Context::scope_id`] method. /// A component's ScopeId can be obtained from `use_hook` or the [`Context::scope_id`] method.
@@ -178,65 +182,33 @@ impl<'src> Context<'src> {
/// struct SharedState(&'static str); /// struct SharedState(&'static str);
/// ///
/// static App: FC<()> = |cx, props|{ /// static App: FC<()> = |cx, props|{
/// cx.use_provide_state(|| SharedState("world")); /// cx.provide_state(SharedState("world"));
/// rsx!(cx, Child {}) /// rsx!(cx, Child {})
/// } /// }
/// ///
/// static Child: FC<()> = |cx, props|{ /// static Child: FC<()> = |cx, props|{
/// let state = cx.use_consume_state::<SharedState>(); /// let state = cx.consume_state::<SharedState>();
/// rsx!(cx, div { "hello {state.0}" }) /// rsx!(cx, div { "hello {state.0}" })
/// } /// }
/// ``` /// ```
pub fn use_provide_state<T, F>(self, init: F) -> &'src Rc<T> pub fn provide_state<T>(self, value: T) -> Option<Rc<T>>
where where
T: 'static, T: 'static,
F: FnOnce() -> T,
{ {
let is_initialized = self.use_hook( self.scope
|_| false, .shared_contexts
|s| { .borrow_mut()
let i = *s; .insert(TypeId::of::<T>(), Rc::new(value))
*s = true; .map(|f| f.downcast::<T>().ok())
i .flatten()
},
|_| {},
);
if !is_initialized {
let existing = self
.scope
.shared_contexts
.borrow_mut()
.insert(TypeId::of::<T>(), Rc::new(init()));
if existing.is_some() {
log::warn!(
"A shared state was replaced with itself. \
This is does not result in a panic, but is probably not what you are trying to do"
);
}
}
self.use_consume_state().unwrap()
} }
/// Uses a context, storing the cached value around /// Try to retrive a SharedState with type T from the any parent Scope.
/// pub fn consume_state<T: 'static>(self) -> Option<Rc<T>> {
/// If a context is not found on the first search, then this call will be "dud", always returning "None" even if a let getter = &self.scope.shared.get_shared_context;
/// context was added later. This allows using another hook as a fallback let ty = TypeId::of::<T>();
/// let idx = self.scope.our_arena_idx;
pub fn use_consume_state<T: 'static>(self) -> Option<&'src Rc<T>> { getter(idx, ty).map(|f| f.downcast().unwrap())
struct UseContextHook<C>(Option<Rc<C>>);
self.use_hook(
move |_| {
let getter = &self.scope.shared.get_shared_context;
let ty = TypeId::of::<T>();
let idx = self.scope.our_arena_idx;
UseContextHook(getter(idx, ty).map(|f| f.downcast().unwrap()))
},
move |hook| hook.0.as_ref(),
|_| {},
)
} }
/// Create a new subtree with this scope as the root of the subtree. /// Create a new subtree with this scope as the root of the subtree.
@@ -250,15 +222,11 @@ impl<'src> Context<'src> {
/// ///
/// ```rust /// ```rust
/// static App: FC<()> = |cx, props| { /// static App: FC<()> = |cx, props| {
/// let id = cx.get_current_subtree(); /// todo!();
/// let id = cx.use_create_subtree();
/// subtree {
///
/// }
/// rsx!(cx, div { "Subtree {id}"}) /// rsx!(cx, div { "Subtree {id}"})
/// }; /// };
/// ``` /// ```
pub fn use_create_subtree(self) -> Option<u32> { pub fn create_subtree(self) -> Option<u32> {
self.scope.new_subtree() self.scope.new_subtree()
} }
+16 -33
View File
@@ -326,7 +326,7 @@ impl<'bump> DiffMachine<'bump> {
} }
for attr in *attributes { for attr in *attributes {
self.mutations.set_attribute(attr); self.mutations.set_attribute(attr, real_id.as_u64());
} }
if !children.is_empty() { if !children.is_empty() {
@@ -428,9 +428,7 @@ impl<'bump> DiffMachine<'bump> {
fn diff_text_nodes(&mut self, old: &'bump VText<'bump>, new: &'bump VText<'bump>) { fn diff_text_nodes(&mut self, old: &'bump VText<'bump>, new: &'bump VText<'bump>) {
if let Some(root) = old.dom_id.get() { if let Some(root) = old.dom_id.get() {
if old.text != new.text { if old.text != new.text {
self.mutations.push_root(root); self.mutations.set_text(new.text, root.as_u64());
self.mutations.set_text(new.text);
self.mutations.pop();
} }
new.dom_id.set(Some(root)); new.dom_id.set(Some(root));
@@ -443,7 +441,7 @@ impl<'bump> DiffMachine<'bump> {
new: &'bump VElement<'bump>, new: &'bump VElement<'bump>,
new_node: &'bump VNode<'bump>, new_node: &'bump VNode<'bump>,
) { ) {
let root = old.dom_id.get(); let root = old.dom_id.get().unwrap();
// If the element type is completely different, the element needs to be re-rendered completely // If the element type is completely different, the element needs to be re-rendered completely
// This is an optimization React makes due to how users structure their code // This is an optimization React makes due to how users structure their code
@@ -462,24 +460,13 @@ impl<'bump> DiffMachine<'bump> {
return; return;
} }
new.dom_id.set(root); new.dom_id.set(Some(root));
// todo: attributes currently rely on the element on top of the stack, but in theory, we only need the id of the // todo: attributes currently rely on the element on top of the stack, but in theory, we only need the id of the
// element to modify its attributes. // element to modify its attributes.
// it would result in fewer instructions if we just set the id directly. // it would result in fewer instructions if we just set the id directly.
// it would also clean up this code some, but that's not very important anyways // it would also clean up this code some, but that's not very important anyways
// Don't push the root if we don't have to
let mut has_comitted = false;
let mut please_commit = |edits: &mut Vec<DomEdit>| {
if !has_comitted {
has_comitted = true;
edits.push(PushRoot {
root: root.unwrap().as_u64(),
});
}
};
// Diff Attributes // Diff Attributes
// //
// It's extraordinarily rare to have the number/order of attributes change // It's extraordinarily rare to have the number/order of attributes change
@@ -489,20 +476,17 @@ impl<'bump> DiffMachine<'bump> {
if old.attributes.len() == new.attributes.len() { if old.attributes.len() == new.attributes.len() {
for (old_attr, new_attr) in old.attributes.iter().zip(new.attributes.iter()) { for (old_attr, new_attr) in old.attributes.iter().zip(new.attributes.iter()) {
if old_attr.value != new_attr.value { if old_attr.value != new_attr.value {
please_commit(&mut self.mutations.edits); self.mutations.set_attribute(new_attr, root.as_u64());
self.mutations.set_attribute(new_attr);
} else if new_attr.is_volatile { } else if new_attr.is_volatile {
please_commit(&mut self.mutations.edits); self.mutations.set_attribute(new_attr, root.as_u64());
self.mutations.set_attribute(new_attr);
} }
} }
} else { } else {
please_commit(&mut self.mutations.edits);
for attribute in old.attributes { for attribute in old.attributes {
self.mutations.remove_attribute(attribute); self.mutations.remove_attribute(attribute, root.as_u64());
} }
for attribute in new.attributes { for attribute in new.attributes {
self.mutations.set_attribute(attribute) self.mutations.set_attribute(attribute, root.as_u64())
} }
} }
@@ -520,20 +504,20 @@ impl<'bump> DiffMachine<'bump> {
if old.listeners.len() == new.listeners.len() { if old.listeners.len() == new.listeners.len() {
for (old_l, new_l) in old.listeners.iter().zip(new.listeners.iter()) { for (old_l, new_l) in old.listeners.iter().zip(new.listeners.iter()) {
if old_l.event != new_l.event { if old_l.event != new_l.event {
please_commit(&mut self.mutations.edits); self.mutations
self.mutations.remove_event_listener(old_l.event); .remove_event_listener(old_l.event, root.as_u64());
self.mutations.new_event_listener(new_l, cur_scope_id); self.mutations.new_event_listener(new_l, cur_scope_id);
} }
new_l.mounted_node.set(old_l.mounted_node.get()); new_l.mounted_node.set(old_l.mounted_node.get());
self.attach_listener_to_scope(new_l, scope); self.attach_listener_to_scope(new_l, scope);
} }
} else { } else {
please_commit(&mut self.mutations.edits);
for listener in old.listeners { for listener in old.listeners {
self.mutations.remove_event_listener(listener.event); self.mutations
.remove_event_listener(listener.event, root.as_u64());
} }
for listener in new.listeners { for listener in new.listeners {
listener.mounted_node.set(root); listener.mounted_node.set(Some(root));
self.mutations.new_event_listener(listener, cur_scope_id); self.mutations.new_event_listener(listener, cur_scope_id);
self.attach_listener_to_scope(listener, scope); self.attach_listener_to_scope(listener, scope);
} }
@@ -541,13 +525,12 @@ impl<'bump> DiffMachine<'bump> {
} }
if old.children.len() == 0 && new.children.len() != 0 { if old.children.len() == 0 && new.children.len() != 0 {
please_commit(&mut self.mutations.edits); self.mutations.edits.push(PushRoot {
root: root.as_u64(),
});
self.stack.create_children(new.children, MountType::Append); self.stack.create_children(new.children, MountType::Append);
} else { } else {
self.diff_children(old.children, new.children); self.diff_children(old.children, new.children);
if has_comitted {
self.mutations.pop();
}
} }
} }
+12 -7
View File
@@ -90,16 +90,16 @@ impl<'a> Mutations<'a> {
root: element_id, root: element_id,
}); });
} }
pub(crate) fn remove_event_listener(&mut self, event: &'static str) { pub(crate) fn remove_event_listener(&mut self, event: &'static str, root: u64) {
self.edits.push(RemoveEventListener { event }); self.edits.push(RemoveEventListener { event, root });
} }
// modify // modify
pub(crate) fn set_text(&mut self, text: &'a str) { pub(crate) fn set_text(&mut self, text: &'a str, root: u64) {
self.edits.push(SetText { text }); self.edits.push(SetText { text, root });
} }
pub(crate) fn set_attribute(&mut self, attribute: &'a Attribute) { pub(crate) fn set_attribute(&mut self, attribute: &'a Attribute, root: u64) {
let Attribute { let Attribute {
name, name,
value, value,
@@ -111,12 +111,13 @@ impl<'a> Mutations<'a> {
field: name, field: name,
value, value,
ns: *namespace, ns: *namespace,
root,
}); });
} }
pub(crate) fn remove_attribute(&mut self, attribute: &Attribute) { pub(crate) fn remove_attribute(&mut self, attribute: &Attribute, root: u64) {
let name = attribute.name; let name = attribute.name;
self.edits.push(RemoveAttribute { name }); self.edits.push(RemoveAttribute { name, root });
} }
} }
@@ -206,17 +207,21 @@ pub enum DomEdit<'bump> {
root: u64, root: u64,
}, },
RemoveEventListener { RemoveEventListener {
root: u64,
event: &'static str, event: &'static str,
}, },
SetText { SetText {
root: u64,
text: &'bump str, text: &'bump str,
}, },
SetAttribute { SetAttribute {
root: u64,
field: &'static str, field: &'static str,
value: &'bump str, value: &'bump str,
ns: Option<&'bump str>, ns: Option<&'bump str>,
}, },
RemoveAttribute { RemoveAttribute {
root: u64,
name: &'static str, name: &'static str,
}, },
} }
+4 -7
View File
@@ -45,13 +45,10 @@ fn html_and_rsx_generate_the_same_output() {
assert_eq!( assert_eq!(
change.edits, change.edits,
[ [SetText {
PushRoot { root: 1 }, text: "Goodbye world",
SetText { root: 1
text: "Goodbye world" },]
},
PopRoot
]
); );
} }
+2 -2
View File
@@ -18,12 +18,12 @@ fn shared_state_test() {
struct MySharedState(&'static str); struct MySharedState(&'static str);
static App: FC<()> = |cx, props| { static App: FC<()> = |cx, props| {
cx.use_provide_state(|| MySharedState("world!")); cx.provide_state(MySharedState("world!"));
rsx!(cx, Child {}) rsx!(cx, Child {})
}; };
static Child: FC<()> = |cx, props| { static Child: FC<()> = |cx, props| {
let shared = cx.use_consume_state::<MySharedState>()?; let shared = cx.consume_state::<MySharedState>()?;
rsx!(cx, "Hello, {shared.0}") rsx!(cx, "Hello, {shared.0}")
}; };
+1 -1
View File
@@ -54,7 +54,7 @@ pub struct WebviewWindowProps<'a> {
/// ///
/// ///
pub fn WebviewWindow<'a>(cx: Context<'a>, props: &'a WebviewWindowProps) -> DomTree<'a> { pub fn WebviewWindow<'a>(cx: Context<'a>, props: &'a WebviewWindowProps) -> DomTree<'a> {
let dtcx = cx.use_consume_state::<RefCell<DesktopContext>>()?; let dtcx = cx.consume_state::<RefCell<DesktopContext>>()?;
cx.use_hook( cx.use_hook(
|_| { |_| {
+3
View File
@@ -3,3 +3,6 @@ pub use usestate::{use_state, AsyncUseState, UseState};
mod useref; mod useref;
pub use useref::*; pub use useref::*;
mod use_shared_state;
pub use use_shared_state::*;
+176
View File
@@ -0,0 +1,176 @@
use dioxus_core::{prelude::Context, ScopeId};
use std::{
cell::{Cell, Ref, RefCell, RefMut},
collections::HashSet,
rc::Rc,
};
type ProvidedState<T> = RefCell<ProvidedStateInner<T>>;
// Tracks all the subscribers to a shared State
pub(crate) struct ProvidedStateInner<T> {
value: Rc<RefCell<T>>,
notify_any: Rc<dyn Fn(ScopeId)>,
consumers: HashSet<ScopeId>,
}
impl<T> ProvidedStateInner<T> {
pub(crate) fn notify_consumers(&mut self) {
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
/// ergonimics in a pinch, with zero cost.
///
/// # Example
///
/// ## Provider
///
/// ```rust
///
///
/// ```
///
/// ## Consumer
///
/// ```rust
///
///
/// ```
///
/// # 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.
///
///
///
pub fn use_shared_state<'a, T: 'static>(cx: Context<'a>) -> Option<UseSharedState<'a, T>> {
cx.use_hook(
|_| {
let scope_id = cx.scope_id();
let root = cx.consume_state::<ProvidedState<T>>();
if let Some(root) = root.as_ref() {
root.borrow_mut().consumers.insert(scope_id);
}
let value = root.as_ref().map(|f| f.borrow().value.clone());
SharedStateInner {
root,
value,
scope_id,
needs_notification: Cell::new(false),
}
},
|f| {
//
f.needs_notification.set(false);
match (&f.value, &f.root) {
(Some(value), Some(root)) => Some(UseSharedState {
cx,
value,
root,
needs_notification: &f.needs_notification,
}),
_ => None,
}
},
|f| {
// we need to unsubscribe when our component is unounted
if let Some(root) = &f.root {
let mut root = root.borrow_mut();
root.consumers.remove(&f.scope_id);
}
},
)
}
struct SharedStateInner<T: 'static> {
root: Option<Rc<ProvidedState<T>>>,
value: Option<Rc<RefCell<T>>>,
scope_id: ScopeId,
needs_notification: Cell<bool>,
}
pub struct UseSharedState<'a, T: 'static> {
pub(crate) cx: Context<'a>,
pub(crate) value: &'a Rc<RefCell<T>>,
pub(crate) root: &'a Rc<RefCell<ProvidedStateInner<T>>>,
pub(crate) needs_notification: &'a Cell<bool>,
}
impl<'a, T: 'static> UseSharedState<'a, T> {
pub fn read(&self) -> Ref<'_, T> {
self.value.borrow()
}
pub fn notify_consumers(self) {
if !self.needs_notification.get() {
self.needs_notification.set(true);
self.root.borrow_mut().notify_consumers();
}
}
pub fn read_write(&self) -> (Ref<'_, T>, &Self) {
(self.read(), 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
pub fn write(&self) -> RefMut<'_, T> {
self.cx.needs_update();
self.notify_consumers();
self.value.borrow_mut()
}
/// Allows the ability to write the value without forcing a re-render
pub fn write_silent(&self) -> RefMut<'_, T> {
self.value.borrow_mut()
}
}
impl<T> Copy for UseSharedState<'_, T> {}
impl<'a, T> Clone for UseSharedState<'a, T>
where
T: 'static,
{
fn clone(&self) -> Self {
UseSharedState {
cx: self.cx,
value: self.value,
root: self.root,
needs_notification: self.needs_notification,
}
}
}
/// Provide some state for components down the hierarchy to consume without having to drill props.
///
///
///
///
///
///
///
pub fn use_provide_state<'a, T: 'static>(cx: Context<'a>, f: impl FnOnce() -> T) -> Option<()> {
cx.use_hook(
|_| {
cx.provide_state(ProvidedStateInner {
value: Rc::new(RefCell::new(f())),
notify_any: cx.schedule_update_any(),
consumers: HashSet::new(),
})
},
|inner| inner.as_ref().and_then(|_| Some(())),
|_| {},
)
}
+1 -1
View File
@@ -2,7 +2,7 @@ use dioxus_core::prelude::Context;
use std::{ use std::{
cell::{Cell, Ref, RefCell, RefMut}, cell::{Cell, Ref, RefCell, RefMut},
fmt::Display, fmt::Display,
ops::{Deref, DerefMut, Not}, ops::Not,
rc::Rc, rc::Rc,
}; };
+2
View File
@@ -518,6 +518,8 @@ pub trait GlobalAttributes {
/// Specifies how an element is positioned. /// Specifies how an element is positioned.
position: "position", position: "position",
pointer_events: "pointer-events",
/// Specifies quotation marks for embedded quotations. /// Specifies quotation marks for embedded quotations.
quotes: "quotes", quotes: "quotes",
+1 -1
View File
@@ -1,9 +1,9 @@
//! Basic example that renders a simple VNode to the browser. //! Basic example that renders a simple VNode to the browser.
use dioxus_core as dioxus; use dioxus_core as dioxus;
use dioxus_core::prelude::*; use dioxus_core::prelude::*;
use dioxus_core_macro::*;
use dioxus_hooks::*; use dioxus_hooks::*;
use dioxus_html as dioxus_elements; use dioxus_html as dioxus_elements;
use dioxus_core_macro::*;
fn main() { fn main() {
console_error_panic_hook::set_once(); console_error_panic_hook::set_once();
+40 -15
View File
@@ -122,11 +122,18 @@ impl WebsysDom {
root: mounted_node_id, root: mounted_node_id,
} => self.new_event_listener(event_name, scope, mounted_node_id), } => self.new_event_listener(event_name, scope, mounted_node_id),
DomEdit::RemoveEventListener { event } => self.remove_event_listener(event), DomEdit::RemoveEventListener { event, root } => {
self.remove_event_listener(event, root)
}
DomEdit::SetText { text } => self.set_text(text), DomEdit::SetText { text, root } => self.set_text(text, root),
DomEdit::SetAttribute { field, value, ns } => self.set_attribute(field, value, ns), DomEdit::SetAttribute {
DomEdit::RemoveAttribute { name } => self.remove_attribute(name), field,
value,
ns,
root,
} => self.set_attribute(field, value, ns, root),
DomEdit::RemoveAttribute { name, root } => self.remove_attribute(name, root),
DomEdit::InsertAfter { n, root } => self.insert_after(n, root), DomEdit::InsertAfter { n, root } => self.insert_after(n, root),
DomEdit::InsertBefore { n, root } => self.insert_before(n, root), DomEdit::InsertBefore { n, root } => self.insert_before(n, root),
@@ -302,16 +309,17 @@ impl WebsysDom {
} }
} }
fn remove_event_listener(&mut self, event: &str) { fn remove_event_listener(&mut self, event: &str, root: u64) {
// todo!() // todo!()
} }
fn set_text(&mut self, text: &str) { fn set_text(&mut self, text: &str, root: u64) {
self.stack.top().set_text_content(Some(text)) let el = self.nodes[root as usize].as_ref().unwrap();
el.set_text_content(Some(text))
} }
fn set_attribute(&mut self, name: &str, value: &str, ns: Option<&str>) { fn set_attribute(&mut self, name: &str, value: &str, ns: Option<&str>, root: u64) {
let node = self.stack.top(); let node = self.nodes[root as usize].as_ref().unwrap();
if ns == Some("style") { if ns == Some("style") {
if let Some(el) = node.dyn_ref::<Element>() { if let Some(el) = node.dyn_ref::<Element>() {
let el = el.dyn_ref::<HtmlElement>().unwrap(); let el = el.dyn_ref::<HtmlElement>().unwrap();
@@ -345,7 +353,11 @@ impl WebsysDom {
} }
"checked" => { "checked" => {
if let Some(input) = node.dyn_ref::<HtmlInputElement>() { if let Some(input) = node.dyn_ref::<HtmlInputElement>() {
input.set_checked(true); match value {
"true" => input.set_checked(true),
"false" => input.set_checked(false),
_ => fallback(),
}
} else { } else {
fallback(); fallback();
} }
@@ -362,8 +374,8 @@ impl WebsysDom {
} }
} }
fn remove_attribute(&mut self, name: &str) { fn remove_attribute(&mut self, name: &str, root: u64) {
let node = self.stack.top(); let node = self.nodes[root as usize].as_ref().unwrap();
if let Some(node) = node.dyn_ref::<web_sys::Element>() { if let Some(node) = node.dyn_ref::<web_sys::Element>() {
node.remove_attribute(name).unwrap(); node.remove_attribute(name).unwrap();
} }
@@ -510,17 +522,30 @@ fn virtual_event_from_websys_event(event: web_sys::Event) -> SyntheticEvent {
FocusEventInner {}, FocusEventInner {},
DioxusWebsysEvent(event), DioxusWebsysEvent(event),
))), ))),
"change" => SyntheticEvent::GenericEvent(DioxusEvent::new((), DioxusWebsysEvent(event))), // "change" => SyntheticEvent::GenericEvent(DioxusEvent::new((), DioxusWebsysEvent(event))),
// todo: these handlers might get really slow if the input box gets large and allocation pressure is heavy // todo: these handlers might get really slow if the input box gets large and allocation pressure is heavy
// don't have a good solution with the serialized event problem // don't have a good solution with the serialized event problem
"input" | "invalid" | "reset" | "submit" => { "change" | "input" | "invalid" | "reset" | "submit" => {
let evt: &web_sys::Event = event.dyn_ref().unwrap(); let evt: &web_sys::Event = event.dyn_ref().unwrap();
let target: web_sys::EventTarget = evt.target().unwrap(); let target: web_sys::EventTarget = evt.target().unwrap();
let value: String = (&target) let value: String = (&target)
.dyn_ref() .dyn_ref()
.map(|input: &web_sys::HtmlInputElement| input.value()) .map(|input: &web_sys::HtmlInputElement| {
// todo: special case more input types
match input.type_().as_str() {
"checkbox" => {
match input.checked() {
true => "true".to_string(),
false => "false".to_string(),
}
},
_ => {
input.value()
}
}
})
.or_else(|| { .or_else(|| {
target target
.dyn_ref() .dyn_ref()