mirror of
https://github.com/DioxusLabs/dioxus
synced 2024-11-10 06:34:20 +00:00
Deduplicate reactive scope updates/Reset subscriptions on reruns/fix use memo double update (#2506)
* deduplicate effect updates * only subscribe to signals read in the current run of reactive scopes * subscribe to memo reads after recomputing the value
This commit is contained in:
parent
24d247dc85
commit
b6dc2a2230
18 changed files with 461 additions and 252 deletions
1
Cargo.lock
generated
1
Cargo.lock
generated
|
@ -2323,6 +2323,7 @@ dependencies = [
|
|||
"quote",
|
||||
"rustversion",
|
||||
"syn 2.0.60",
|
||||
"tokio",
|
||||
"trybuild",
|
||||
]
|
||||
|
||||
|
|
|
@ -24,6 +24,7 @@ prettyplease = "0.2.15"
|
|||
[dev-dependencies]
|
||||
dioxus = { workspace = true }
|
||||
rustversion = "1.0"
|
||||
tokio = { workspace = true, features = ["full", "time"] }
|
||||
trybuild = "1.0"
|
||||
|
||||
[features]
|
||||
|
|
|
@ -2,8 +2,8 @@ use dioxus::prelude::*;
|
|||
use dioxus_core::ElementId;
|
||||
use std::rc::Rc;
|
||||
|
||||
#[test]
|
||||
fn values_memoize_in_place() {
|
||||
#[tokio::test]
|
||||
async fn values_memoize_in_place() {
|
||||
thread_local! {
|
||||
static DROP_COUNT: std::cell::RefCell<usize> = const { std::cell::RefCell::new(0) };
|
||||
}
|
||||
|
@ -20,9 +20,14 @@ fn values_memoize_in_place() {
|
|||
let mut count = use_signal(|| 0);
|
||||
let x = CountsDrop;
|
||||
|
||||
if generation() < 15 {
|
||||
count += 1;
|
||||
}
|
||||
use_hook(|| {
|
||||
spawn(async move {
|
||||
for _ in 0..15 {
|
||||
tokio::time::sleep(std::time::Duration::from_millis(10)).await;
|
||||
count += 1;
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
rsx! {
|
||||
TakesEventHandler {
|
||||
|
@ -33,7 +38,6 @@ fn values_memoize_in_place() {
|
|||
},
|
||||
children: count() / 2
|
||||
}
|
||||
TakesSignal { sig: count(), children: count() / 2 }
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -50,12 +54,16 @@ fn values_memoize_in_place() {
|
|||
ElementId(1),
|
||||
true,
|
||||
);
|
||||
tokio::select! {
|
||||
_ = tokio::time::sleep(std::time::Duration::from_millis(20)) => {},
|
||||
_ = dom.wait_for_work() => {}
|
||||
}
|
||||
dom.render_immediate(&mut dioxus_core::NoOpMutations);
|
||||
}
|
||||
dom.render_immediate(&mut dioxus_core::NoOpMutations);
|
||||
// As we rerun the app, the drop count should be 15 one for each render of the app component
|
||||
let drop_count = DROP_COUNT.with(|c| *c.borrow());
|
||||
assert_eq!(drop_count, 15);
|
||||
assert_eq!(drop_count, 16);
|
||||
}
|
||||
|
||||
// We move over event handlers in place. Make sure we do that in a way that doesn't destroy the original event handler
|
||||
|
@ -98,6 +106,7 @@ fn cloning_event_handler_components_work() {
|
|||
|
||||
#[component]
|
||||
fn TakesEventHandler(click: EventHandler<usize>, children: usize) -> Element {
|
||||
println!("children is{children}");
|
||||
let first_render_click = use_hook(move || click);
|
||||
if generation() > 0 {
|
||||
// Make sure the event handler is memoized in place and never gets dropped
|
||||
|
|
|
@ -9,7 +9,7 @@ use std::{
|
|||
|
||||
use generational_box::{AnyStorage, Owner, SyncStorage, UnsyncStorage};
|
||||
|
||||
use crate::{innerlude::current_scope_id, ScopeId};
|
||||
use crate::{innerlude::current_scope_id, Runtime, ScopeId};
|
||||
|
||||
/// Run a closure with the given owner.
|
||||
///
|
||||
|
@ -91,12 +91,6 @@ pub fn current_owner<S: AnyStorage>() -> Owner<S> {
|
|||
impl ScopeId {
|
||||
/// Get the owner for the current scope.
|
||||
pub fn owner<S: AnyStorage>(self) -> Owner<S> {
|
||||
match self.has_context() {
|
||||
Some(rt) => rt,
|
||||
None => {
|
||||
let owner = S::owner();
|
||||
self.provide_context(owner)
|
||||
}
|
||||
}
|
||||
Runtime::with_scope(self, |cx| cx.owner::<S>()).expect("to be in a dioxus runtime")
|
||||
}
|
||||
}
|
||||
|
|
|
@ -15,6 +15,7 @@ mod global_context;
|
|||
mod mutations;
|
||||
mod nodes;
|
||||
mod properties;
|
||||
mod reactive_context;
|
||||
mod render_signal;
|
||||
mod runtime;
|
||||
mod scheduler;
|
||||
|
@ -42,6 +43,7 @@ pub(crate) mod innerlude {
|
|||
pub use crate::mutations::*;
|
||||
pub use crate::nodes::*;
|
||||
pub use crate::properties::*;
|
||||
pub use crate::reactive_context::*;
|
||||
pub use crate::runtime::{Runtime, RuntimeGuard};
|
||||
pub use crate::scheduler::*;
|
||||
pub use crate::scopes::*;
|
||||
|
@ -79,7 +81,7 @@ pub mod prelude {
|
|||
use_hook_with_cleanup, wait_for_next_render, with_owner, AnyValue, Attribute, Callback,
|
||||
Component, ComponentFunction, Element, ErrorBoundary, Event, EventHandler, Fragment,
|
||||
HasAttributes, IntoAttributeValue, IntoDynNode, OptionStringFromMarker, Properties,
|
||||
Runtime, RuntimeGuard, ScopeId, ScopeState, SuperFrom, SuperInto, Task, Template,
|
||||
TemplateAttribute, TemplateNode, Throw, VNode, VNodeInner, VirtualDom,
|
||||
ReactiveContext, Runtime, RuntimeGuard, ScopeId, ScopeState, SuperFrom, SuperInto, Task,
|
||||
Template, TemplateAttribute, TemplateNode, Throw, VNode, VNodeInner, VirtualDom,
|
||||
};
|
||||
}
|
||||
|
|
298
packages/core/src/reactive_context.rs
Normal file
298
packages/core/src/reactive_context.rs
Normal file
|
@ -0,0 +1,298 @@
|
|||
use crate::{
|
||||
prelude::{current_scope_id, ScopeId},
|
||||
scope_context::Scope,
|
||||
tasks::SchedulerMsg,
|
||||
Runtime,
|
||||
};
|
||||
use futures_channel::mpsc::UnboundedReceiver;
|
||||
use generational_box::{GenerationalBox, SyncStorage};
|
||||
use std::{
|
||||
cell::RefCell,
|
||||
collections::HashSet,
|
||||
hash::Hash,
|
||||
sync::{Arc, Mutex},
|
||||
};
|
||||
|
||||
#[doc = include_str!("../docs/reactivity.md")]
|
||||
#[derive(Clone, Copy)]
|
||||
pub struct ReactiveContext {
|
||||
scope: ScopeId,
|
||||
inner: GenerationalBox<Inner, SyncStorage>,
|
||||
}
|
||||
|
||||
impl PartialEq for ReactiveContext {
|
||||
fn eq(&self, other: &Self) -> bool {
|
||||
self.inner.ptr_eq(&other.inner)
|
||||
}
|
||||
}
|
||||
|
||||
impl Eq for ReactiveContext {}
|
||||
|
||||
thread_local! {
|
||||
static CURRENT: RefCell<Vec<ReactiveContext>> = const { RefCell::new(vec![]) };
|
||||
}
|
||||
|
||||
impl std::fmt::Display for ReactiveContext {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
#[cfg(debug_assertions)]
|
||||
{
|
||||
if let Ok(read) = self.inner.try_read() {
|
||||
if let Some(scope) = read.scope {
|
||||
return write!(f, "ReactiveContext(for scope: {:?})", scope);
|
||||
}
|
||||
return write!(f, "ReactiveContext created at {}", read.origin);
|
||||
}
|
||||
}
|
||||
write!(f, "ReactiveContext")
|
||||
}
|
||||
}
|
||||
|
||||
impl ReactiveContext {
|
||||
/// Create a new reactive context
|
||||
#[track_caller]
|
||||
pub fn new() -> (Self, UnboundedReceiver<()>) {
|
||||
Self::new_with_origin(std::panic::Location::caller())
|
||||
}
|
||||
|
||||
/// Create a new reactive context with a location for debugging purposes
|
||||
/// This is useful for reactive contexts created within closures
|
||||
pub fn new_with_origin(
|
||||
origin: &'static std::panic::Location<'static>,
|
||||
) -> (Self, UnboundedReceiver<()>) {
|
||||
let (tx, rx) = futures_channel::mpsc::unbounded();
|
||||
let callback = move || {
|
||||
// If there is already an update queued, we don't need to queue another
|
||||
if !tx.is_empty() {
|
||||
return;
|
||||
}
|
||||
let _ = tx.unbounded_send(());
|
||||
};
|
||||
let _self = Self::new_with_callback(callback, current_scope_id().unwrap(), origin);
|
||||
(_self, rx)
|
||||
}
|
||||
|
||||
/// Create a new reactive context that may update a scope. When any signal that this context subscribes to changes, the callback will be run
|
||||
pub fn new_with_callback(
|
||||
callback: impl FnMut() + Send + Sync + 'static,
|
||||
scope: ScopeId,
|
||||
#[allow(unused)] origin: &'static std::panic::Location<'static>,
|
||||
) -> Self {
|
||||
let inner = Inner {
|
||||
self_: None,
|
||||
update: Box::new(callback),
|
||||
subscribers: Default::default(),
|
||||
#[cfg(debug_assertions)]
|
||||
origin,
|
||||
#[cfg(debug_assertions)]
|
||||
scope: None,
|
||||
};
|
||||
|
||||
let owner = scope.owner();
|
||||
|
||||
let self_ = Self {
|
||||
scope,
|
||||
inner: owner.insert(inner),
|
||||
};
|
||||
|
||||
self_.inner.write().self_ = Some(self_);
|
||||
|
||||
self_
|
||||
}
|
||||
|
||||
/// Get the current reactive context from the nearest reactive hook or scope
|
||||
pub fn current() -> Option<Self> {
|
||||
CURRENT.with(|current| current.borrow().last().cloned())
|
||||
}
|
||||
|
||||
/// Create a reactive context for a scope id
|
||||
pub(crate) fn new_for_scope(scope: &Scope, runtime: &Runtime) -> Self {
|
||||
let id = scope.id;
|
||||
let sender = runtime.sender.clone();
|
||||
let update_scope = move || {
|
||||
tracing::trace!("Marking scope {:?} as dirty", id);
|
||||
sender.unbounded_send(SchedulerMsg::Immediate(id)).unwrap();
|
||||
};
|
||||
|
||||
// Otherwise, create a new context at the current scope
|
||||
let inner = Inner {
|
||||
self_: None,
|
||||
update: Box::new(update_scope),
|
||||
subscribers: Default::default(),
|
||||
#[cfg(debug_assertions)]
|
||||
origin: std::panic::Location::caller(),
|
||||
#[cfg(debug_assertions)]
|
||||
scope: Some(id),
|
||||
};
|
||||
|
||||
let owner = scope.owner();
|
||||
|
||||
let self_ = Self {
|
||||
scope: id,
|
||||
inner: owner.insert(inner),
|
||||
};
|
||||
|
||||
self_.inner.write().self_ = Some(self_);
|
||||
|
||||
self_
|
||||
}
|
||||
|
||||
/// Clear all subscribers to this context
|
||||
pub fn clear_subscribers(&self) {
|
||||
let old_subscribers = std::mem::take(&mut self.inner.write().subscribers);
|
||||
for subscriber in old_subscribers {
|
||||
subscriber.0.lock().unwrap().remove(self);
|
||||
}
|
||||
}
|
||||
|
||||
/// Update the subscribers
|
||||
pub(crate) fn update_subscribers(&self) {
|
||||
let subscribers = &self.inner.read().subscribers;
|
||||
for subscriber in subscribers.iter() {
|
||||
subscriber.0.lock().unwrap().insert(*self);
|
||||
}
|
||||
}
|
||||
|
||||
/// Reset the reactive context and then run the callback in the context. This can be used to create custom reactive hooks like `use_memo`.
|
||||
///
|
||||
/// ```rust, no_run
|
||||
/// # use dioxus::prelude::*;
|
||||
/// # use futures_util::StreamExt;
|
||||
/// fn use_simplified_memo(mut closure: impl FnMut() -> i32 + 'static) -> Signal<i32> {
|
||||
/// use_hook(|| {
|
||||
/// // Create a new reactive context and channel that will recieve a value every time a value the reactive context subscribes to changes
|
||||
/// let (reactive_context, mut changed) = ReactiveContext::new();
|
||||
/// // Compute the value of the memo inside the reactive context. This will subscribe the reactive context to any values you read inside the closure
|
||||
/// let value = reactive_context.reset_and_run_in(&mut closure);
|
||||
/// // Create a new signal with the value of the memo
|
||||
/// let mut signal = Signal::new(value);
|
||||
/// // Create a task that reruns the closure when the reactive context changes
|
||||
/// spawn(async move {
|
||||
/// while changed.next().await.is_some() {
|
||||
/// // Since we reset the reactive context as we run the closure, our memo will only subscribe to the new values that are read in the closure
|
||||
/// let new_value = reactive_context.run_in(&mut closure);
|
||||
/// if new_value != value {
|
||||
/// signal.set(new_value);
|
||||
/// }
|
||||
/// }
|
||||
/// });
|
||||
/// signal
|
||||
/// })
|
||||
/// }
|
||||
///
|
||||
/// let mut boolean = use_signal(|| false);
|
||||
/// let mut count = use_signal(|| 0);
|
||||
/// // Because we use `reset_and_run_in` instead of just `run_in`, our memo will only subscribe to the signals that are read this run of the closure (initially just the boolean)
|
||||
/// let memo = use_simplified_memo(move || if boolean() { count() } else { 0 });
|
||||
/// println!("{memo}");
|
||||
/// // Because the count signal is not read in this run of the closure, the memo will not rerun
|
||||
/// count += 1;
|
||||
/// println!("{memo}");
|
||||
/// // Because the boolean signal is read in this run of the closure, the memo will rerun
|
||||
/// boolean.toggle();
|
||||
/// println!("{memo}");
|
||||
/// // If we toggle the boolean again, and the memo unsubscribes from the count signal
|
||||
/// boolean.toggle();
|
||||
/// println!("{memo}");
|
||||
/// ```
|
||||
pub fn reset_and_run_in<O>(&self, f: impl FnOnce() -> O) -> O {
|
||||
self.clear_subscribers();
|
||||
self.run_in(f)
|
||||
}
|
||||
|
||||
/// Run this function in the context of this reactive context
|
||||
///
|
||||
/// This will set the current reactive context to this context for the duration of the function.
|
||||
/// You can then get information about the current subscriptions.
|
||||
pub fn run_in<O>(&self, f: impl FnOnce() -> O) -> O {
|
||||
CURRENT.with(|current| current.borrow_mut().push(*self));
|
||||
let out = f();
|
||||
CURRENT.with(|current| current.borrow_mut().pop());
|
||||
self.update_subscribers();
|
||||
out
|
||||
}
|
||||
|
||||
/// Marks this reactive context as dirty
|
||||
///
|
||||
/// If there's a scope associated with this context, then it will be marked as dirty too
|
||||
///
|
||||
/// Returns true if the context was marked as dirty, or false if the context has been dropped
|
||||
pub fn mark_dirty(&self) -> bool {
|
||||
if let Ok(mut self_write) = self.inner.try_write() {
|
||||
#[cfg(debug_assertions)]
|
||||
{
|
||||
tracing::trace!(
|
||||
"Marking reactive context created at {} as dirty",
|
||||
self_write.origin
|
||||
);
|
||||
}
|
||||
|
||||
(self_write.update)();
|
||||
|
||||
true
|
||||
} else {
|
||||
false
|
||||
}
|
||||
}
|
||||
|
||||
/// Subscribe to this context. The reactive context will automatically remove itself from the subscriptions when it is reset.
|
||||
pub fn subscribe(&self, subscriptions: Arc<Mutex<HashSet<ReactiveContext>>>) {
|
||||
subscriptions.lock().unwrap().insert(*self);
|
||||
self.inner
|
||||
.write()
|
||||
.subscribers
|
||||
.insert(PointerHash(subscriptions));
|
||||
}
|
||||
|
||||
/// Get the scope that inner CopyValue is associated with
|
||||
pub fn origin_scope(&self) -> ScopeId {
|
||||
self.scope
|
||||
}
|
||||
}
|
||||
|
||||
impl Hash for ReactiveContext {
|
||||
fn hash<H: std::hash::Hasher>(&self, state: &mut H) {
|
||||
self.inner.id().hash(state);
|
||||
}
|
||||
}
|
||||
|
||||
struct PointerHash<T>(Arc<T>);
|
||||
|
||||
impl<T> Hash for PointerHash<T> {
|
||||
fn hash<H: std::hash::Hasher>(&self, state: &mut H) {
|
||||
std::sync::Arc::<T>::as_ptr(&self.0).hash(state);
|
||||
}
|
||||
}
|
||||
|
||||
impl<T> PartialEq for PointerHash<T> {
|
||||
fn eq(&self, other: &Self) -> bool {
|
||||
std::sync::Arc::<T>::as_ptr(&self.0) == std::sync::Arc::<T>::as_ptr(&other.0)
|
||||
}
|
||||
}
|
||||
|
||||
impl<T> Eq for PointerHash<T> {}
|
||||
|
||||
impl<T> Clone for PointerHash<T> {
|
||||
fn clone(&self) -> Self {
|
||||
Self(self.0.clone())
|
||||
}
|
||||
}
|
||||
|
||||
type SubscriberMap = Mutex<HashSet<ReactiveContext>>;
|
||||
|
||||
struct Inner {
|
||||
self_: Option<ReactiveContext>,
|
||||
|
||||
// Futures will call .changed().await
|
||||
update: Box<dyn FnMut() + Send + Sync>,
|
||||
|
||||
// Subscribers to this context
|
||||
subscribers: HashSet<PointerHash<SubscriberMap>>,
|
||||
|
||||
// Debug information for signal subscriptions
|
||||
#[cfg(debug_assertions)]
|
||||
origin: &'static std::panic::Location<'static>,
|
||||
|
||||
#[cfg(debug_assertions)]
|
||||
// The scope that this reactive context is associated with
|
||||
scope: Option<ScopeId>,
|
||||
}
|
|
@ -1,4 +1,5 @@
|
|||
use crate::innerlude::ScopeOrder;
|
||||
use crate::reactive_context::ReactiveContext;
|
||||
use crate::{
|
||||
any_props::{AnyProps, BoxedAnyProps},
|
||||
innerlude::ScopeState,
|
||||
|
@ -17,16 +18,18 @@ impl VirtualDom {
|
|||
let entry = self.scopes.vacant_entry();
|
||||
let id = ScopeId(entry.key());
|
||||
|
||||
let scope_runtime = Scope::new(name, id, parent_id, height);
|
||||
let reactive_context = ReactiveContext::new_for_scope(&scope_runtime, &self.runtime);
|
||||
self.runtime.create_scope(scope_runtime);
|
||||
|
||||
let scope = entry.insert(ScopeState {
|
||||
runtime: self.runtime.clone(),
|
||||
context_id: id,
|
||||
props,
|
||||
last_rendered_node: Default::default(),
|
||||
reactive_context,
|
||||
});
|
||||
|
||||
self.runtime
|
||||
.create_scope(Scope::new(name, id, parent_id, height));
|
||||
|
||||
scope
|
||||
}
|
||||
|
||||
|
@ -52,7 +55,7 @@ impl VirtualDom {
|
|||
let props: &dyn AnyProps = &*scope.props;
|
||||
|
||||
let span = tracing::trace_span!("render", scope = %scope.state().name);
|
||||
span.in_scope(|| props.render())
|
||||
span.in_scope(|| scope.reactive_context.reset_and_run_in(|| props.render()))
|
||||
};
|
||||
|
||||
let context = scope.state();
|
||||
|
|
|
@ -1,4 +1,5 @@
|
|||
use crate::{innerlude::SchedulerMsg, Element, Runtime, ScopeId, Task};
|
||||
use generational_box::{AnyStorage, Owner};
|
||||
use rustc_hash::FxHashSet;
|
||||
use std::{
|
||||
any::Any,
|
||||
|
@ -91,6 +92,17 @@ impl Scope {
|
|||
})
|
||||
}
|
||||
|
||||
/// Get the owner for the current scope.
|
||||
pub fn owner<S: AnyStorage>(&self) -> Owner<S> {
|
||||
match self.has_context() {
|
||||
Some(rt) => rt,
|
||||
None => {
|
||||
let owner = S::owner();
|
||||
self.provide_context(owner)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Return any context of type T if it exists on this scope
|
||||
pub fn has_context<T: 'static + Clone>(&self) -> Option<T> {
|
||||
self.shared_contexts
|
||||
|
|
|
@ -1,5 +1,6 @@
|
|||
use crate::{
|
||||
any_props::BoxedAnyProps, nodes::RenderReturn, runtime::Runtime, scope_context::Scope,
|
||||
any_props::BoxedAnyProps, nodes::RenderReturn, reactive_context::ReactiveContext,
|
||||
runtime::Runtime, scope_context::Scope,
|
||||
};
|
||||
use std::{cell::Ref, rc::Rc};
|
||||
|
||||
|
@ -53,6 +54,7 @@ pub struct ScopeState {
|
|||
pub(crate) context_id: ScopeId,
|
||||
pub(crate) last_rendered_node: Option<RenderReturn>,
|
||||
pub(crate) props: BoxedAnyProps,
|
||||
pub(crate) reactive_context: ReactiveContext,
|
||||
}
|
||||
|
||||
impl Drop for ScopeState {
|
||||
|
|
|
@ -1,5 +1,6 @@
|
|||
use std::{cell::Cell, rc::Rc};
|
||||
|
||||
use dioxus_core::prelude::*;
|
||||
use dioxus_signals::ReactiveContext;
|
||||
use futures_util::StreamExt;
|
||||
|
||||
use crate::use_callback;
|
||||
|
@ -15,12 +16,24 @@ pub fn use_effect(callback: impl FnMut() + 'static) -> Effect {
|
|||
use_hook(|| {
|
||||
// Inside the effect, we track any reads so that we can rerun the effect if a value the effect reads changes
|
||||
let (rc, mut changed) = ReactiveContext::new_with_origin(location);
|
||||
|
||||
// Deduplicate queued effects
|
||||
let effect_queued = Rc::new(Cell::new(false));
|
||||
|
||||
// Spawn a task that will run the effect when:
|
||||
// 1) The component is first run
|
||||
// 2) The effect is rerun due to an async read at any time
|
||||
// 3) The effect is rerun in the same tick that the component is rerun: we need to wait for the component to rerun before we can run the effect again
|
||||
let queue_effect_for_next_render = move || {
|
||||
queue_effect(move || rc.run_in(&*callback));
|
||||
if effect_queued.get() {
|
||||
return;
|
||||
}
|
||||
effect_queued.set(true);
|
||||
let effect_queued = effect_queued.clone();
|
||||
queue_effect(move || {
|
||||
rc.reset_and_run_in(&*callback);
|
||||
effect_queued.set(false);
|
||||
});
|
||||
};
|
||||
|
||||
queue_effect_for_next_render();
|
||||
|
|
|
@ -30,7 +30,7 @@ where
|
|||
|
||||
let cb = use_callback(move || {
|
||||
// Create the user's task
|
||||
let fut = rc.run_in(&mut future);
|
||||
let fut = rc.reset_and_run_in(&mut future);
|
||||
|
||||
// Spawn a wrapper task that polls the inner future and watch its dependencies
|
||||
spawn(async move {
|
||||
|
|
|
@ -50,21 +50,73 @@ async fn memo_updates() {
|
|||
}
|
||||
}
|
||||
|
||||
let mut dom = VirtualDom::new(app);
|
||||
let race = async move {
|
||||
let mut dom = VirtualDom::new(app);
|
||||
|
||||
dom.rebuild_in_place();
|
||||
let mut signal = VEC_SIGNAL.with(|cell| (*cell.borrow()).unwrap());
|
||||
// Wait for the signal to update
|
||||
for _ in 0..2 {
|
||||
dom.wait_for_work().await;
|
||||
dom.render_immediate(&mut dioxus::dioxus_core::NoOpMutations);
|
||||
}
|
||||
assert_eq!(signal(), vec![0, 1, 2, 3, 4, 5]);
|
||||
// Remove each element from the vec
|
||||
for _ in 0..6 {
|
||||
signal.pop();
|
||||
dom.wait_for_work().await;
|
||||
dom.render_immediate(&mut dioxus::dioxus_core::NoOpMutations);
|
||||
println!("Signal: {signal:?}");
|
||||
}
|
||||
dom.rebuild_in_place();
|
||||
let mut signal = VEC_SIGNAL.with(|cell| (*cell.borrow()).unwrap());
|
||||
// Wait for the signal to update
|
||||
for _ in 0..2 {
|
||||
dom.wait_for_work().await;
|
||||
dom.render_immediate(&mut dioxus::dioxus_core::NoOpMutations);
|
||||
}
|
||||
assert_eq!(signal(), vec![0, 1, 2, 3, 4, 5]);
|
||||
// Remove each element from the vec
|
||||
for _ in 0..6 {
|
||||
signal.pop();
|
||||
dom.wait_for_work().await;
|
||||
dom.render_immediate(&mut dioxus::dioxus_core::NoOpMutations);
|
||||
println!("Signal: {signal:?}");
|
||||
}
|
||||
};
|
||||
|
||||
tokio::select! {
|
||||
_ = race => {},
|
||||
_ = tokio::time::sleep(std::time::Duration::from_millis(1000)) => panic!("timed out")
|
||||
};
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn use_memo_only_triggers_one_update() {
|
||||
use dioxus::prelude::*;
|
||||
use std::cell::RefCell;
|
||||
|
||||
thread_local! {
|
||||
static VEC_SIGNAL: RefCell<Vec<usize>> = const { RefCell::new(Vec::new()) };
|
||||
}
|
||||
|
||||
fn app() -> Element {
|
||||
let mut count = use_signal(|| 0);
|
||||
|
||||
let memorized = use_memo(move || dbg!(count() * 2));
|
||||
|
||||
use_memo(move || {
|
||||
println!("reading doubled");
|
||||
let doubled = memorized();
|
||||
VEC_SIGNAL.with_borrow_mut(|v| v.push(doubled))
|
||||
});
|
||||
|
||||
// Writing to count many times in a row should not cause the memo to update other subscribers multiple times
|
||||
use_hook(move || {
|
||||
for _ in 0..10 {
|
||||
count += 1;
|
||||
// Reading the memo each time will trigger the memo to rerun immediately, but the VEC_SIGNAL should still only rerun once
|
||||
println!("doubled {memorized}");
|
||||
}
|
||||
});
|
||||
|
||||
rsx! {}
|
||||
}
|
||||
|
||||
let mut dom = VirtualDom::new(app);
|
||||
dom.rebuild_in_place();
|
||||
|
||||
tokio::select! {
|
||||
_ = dom.wait_for_work() => {},
|
||||
_ = tokio::time::sleep(std::time::Duration::from_millis(100)) => {}
|
||||
};
|
||||
|
||||
dom.render_immediate(&mut dioxus::dioxus_core::NoOpMutations);
|
||||
|
||||
assert_eq!(VEC_SIGNAL.with(|v| v.borrow().clone()), vec![0, 20]);
|
||||
}
|
||||
|
|
|
@ -39,6 +39,3 @@ pub use write::*;
|
|||
|
||||
mod props;
|
||||
pub use props::*;
|
||||
|
||||
mod reactive_context;
|
||||
pub use reactive_context::*;
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
use crate::read_impls;
|
||||
use crate::write::Writable;
|
||||
use crate::{read::Readable, ReactiveContext, ReadableRef, Signal};
|
||||
use crate::{read::Readable, ReadableRef, Signal};
|
||||
use crate::{CopyValue, ReadOnlySignal};
|
||||
use std::{
|
||||
cell::RefCell,
|
||||
|
@ -10,7 +10,7 @@ use std::{
|
|||
|
||||
use dioxus_core::prelude::*;
|
||||
use futures_util::StreamExt;
|
||||
use generational_box::UnsyncStorage;
|
||||
use generational_box::{AnyStorage, UnsyncStorage};
|
||||
|
||||
struct UpdateInformation<T> {
|
||||
dirty: Arc<AtomicBool>,
|
||||
|
@ -53,7 +53,7 @@ impl<T: 'static> Memo<T> {
|
|||
where
|
||||
T: PartialEq,
|
||||
{
|
||||
let dirty = Arc::new(AtomicBool::new(true));
|
||||
let dirty = Arc::new(AtomicBool::new(false));
|
||||
let (tx, mut rx) = futures_channel::mpsc::unbounded();
|
||||
|
||||
let callback = {
|
||||
|
@ -67,7 +67,7 @@ impl<T: 'static> Memo<T> {
|
|||
ReactiveContext::new_with_callback(callback, current_scope_id().unwrap(), location);
|
||||
|
||||
// Create a new signal in that context, wiring up its dependencies and subscribers
|
||||
let mut recompute = move || rc.run_in(&mut f);
|
||||
let mut recompute = move || rc.reset_and_run_in(&mut f);
|
||||
let value = recompute();
|
||||
let recompute = RefCell::new(Box::new(recompute) as Box<dyn FnMut() -> T>);
|
||||
let update = CopyValue::new(UpdateInformation {
|
||||
|
@ -134,24 +134,30 @@ where
|
|||
fn try_read_unchecked(
|
||||
&self,
|
||||
) -> Result<ReadableRef<'static, Self>, generational_box::BorrowError> {
|
||||
let read = self.inner.try_read_unchecked();
|
||||
match read {
|
||||
Ok(r) => {
|
||||
let needs_update = self
|
||||
.update
|
||||
.read()
|
||||
.dirty
|
||||
.swap(false, std::sync::atomic::Ordering::Relaxed);
|
||||
if needs_update {
|
||||
drop(r);
|
||||
self.recompute();
|
||||
self.inner.try_read_unchecked()
|
||||
} else {
|
||||
Ok(r)
|
||||
}
|
||||
// Read the inner generational box instead of the signal so we have more fine grained control over exactly when the subscription happens
|
||||
let read = self.inner.inner.try_read_unchecked()?;
|
||||
|
||||
let needs_update = self
|
||||
.update
|
||||
.read()
|
||||
.dirty
|
||||
.swap(false, std::sync::atomic::Ordering::Relaxed);
|
||||
let result = if needs_update {
|
||||
drop(read);
|
||||
// We shouldn't be subscribed to the value here so we don't trigger the scope we are currently in to rerun even though that scope got the latest value because we synchronously update the value: https://github.com/DioxusLabs/dioxus/issues/2416
|
||||
self.recompute();
|
||||
self.inner.inner.try_read_unchecked()
|
||||
} else {
|
||||
Ok(read)
|
||||
};
|
||||
// Subscribe to the current scope before returning the value
|
||||
if let Ok(read) = &result {
|
||||
if let Some(reactive_context) = ReactiveContext::current() {
|
||||
tracing::trace!("Subscribing to the reactive context {}", reactive_context);
|
||||
reactive_context.subscribe(read.subscribers.clone());
|
||||
}
|
||||
Err(e) => Err(e),
|
||||
}
|
||||
result.map(|read| <UnsyncStorage as AnyStorage>::map(read, |v| &v.value))
|
||||
}
|
||||
|
||||
/// Get the current value of the signal. **Unlike read, this will not subscribe the current scope to the signal which can cause parts of your UI to not update.**
|
||||
|
|
|
@ -1,181 +0,0 @@
|
|||
use dioxus_core::prelude::{
|
||||
current_scope_id, has_context, provide_context, schedule_update_any, ScopeId,
|
||||
};
|
||||
use futures_channel::mpsc::UnboundedReceiver;
|
||||
use generational_box::SyncStorage;
|
||||
use std::{cell::RefCell, hash::Hash};
|
||||
|
||||
use crate::{CopyValue, Writable};
|
||||
|
||||
#[doc = include_str!("../docs/reactivity.md")]
|
||||
#[derive(Clone, Copy, PartialEq, Eq)]
|
||||
pub struct ReactiveContext {
|
||||
inner: CopyValue<Inner, SyncStorage>,
|
||||
}
|
||||
|
||||
thread_local! {
|
||||
static CURRENT: RefCell<Vec<ReactiveContext>> = const { RefCell::new(vec![]) };
|
||||
}
|
||||
|
||||
impl std::fmt::Display for ReactiveContext {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
#[cfg(debug_assertions)]
|
||||
{
|
||||
use crate::Readable;
|
||||
if let Ok(read) = self.inner.try_read() {
|
||||
if let Some(scope) = read.scope {
|
||||
return write!(f, "ReactiveContext(for scope: {:?})", scope);
|
||||
}
|
||||
return write!(f, "ReactiveContext created at {}", read.origin);
|
||||
}
|
||||
}
|
||||
write!(f, "ReactiveContext")
|
||||
}
|
||||
}
|
||||
|
||||
impl ReactiveContext {
|
||||
/// Create a new reactive context
|
||||
#[track_caller]
|
||||
pub fn new() -> (Self, UnboundedReceiver<()>) {
|
||||
Self::new_with_origin(std::panic::Location::caller())
|
||||
}
|
||||
|
||||
/// Create a new reactive context with a location for debugging purposes
|
||||
/// This is useful for reactive contexts created within closures
|
||||
pub fn new_with_origin(
|
||||
origin: &'static std::panic::Location<'static>,
|
||||
) -> (Self, UnboundedReceiver<()>) {
|
||||
let (tx, rx) = futures_channel::mpsc::unbounded();
|
||||
let callback = move || {
|
||||
let _ = tx.unbounded_send(());
|
||||
};
|
||||
let _self = Self::new_with_callback(callback, current_scope_id().unwrap(), origin);
|
||||
(_self, rx)
|
||||
}
|
||||
|
||||
/// Create a new reactive context that may update a scope. When any signal that this context subscribes to changes, the callback will be run
|
||||
pub fn new_with_callback(
|
||||
callback: impl FnMut() + Send + Sync + 'static,
|
||||
scope: ScopeId,
|
||||
#[allow(unused)] origin: &'static std::panic::Location<'static>,
|
||||
) -> Self {
|
||||
let inner = Inner {
|
||||
self_: None,
|
||||
update: Box::new(callback),
|
||||
#[cfg(debug_assertions)]
|
||||
origin,
|
||||
#[cfg(debug_assertions)]
|
||||
scope: None,
|
||||
};
|
||||
|
||||
let mut self_ = Self {
|
||||
inner: CopyValue::new_maybe_sync_in_scope(inner, scope),
|
||||
};
|
||||
|
||||
self_.inner.write().self_ = Some(self_);
|
||||
|
||||
self_
|
||||
}
|
||||
|
||||
/// Get the current reactive context
|
||||
///
|
||||
/// If this was set manually, then that value will be returned.
|
||||
///
|
||||
/// If there's no current reactive context, then a new one will be created for the current scope and returned.
|
||||
pub fn current() -> Option<Self> {
|
||||
let cur = CURRENT.with(|current| current.borrow().last().cloned());
|
||||
|
||||
// If we're already inside a reactive context, then return that
|
||||
if let Some(cur) = cur {
|
||||
return Some(cur);
|
||||
}
|
||||
|
||||
// If we're rendering, then try and use the reactive context attached to this component
|
||||
if !dioxus_core::vdom_is_rendering() {
|
||||
return None;
|
||||
}
|
||||
if let Some(cx) = has_context() {
|
||||
return Some(cx);
|
||||
}
|
||||
let update_any = schedule_update_any();
|
||||
let scope_id = current_scope_id().unwrap();
|
||||
let update_scope = move || {
|
||||
tracing::trace!("Marking scope {:?} as dirty", scope_id);
|
||||
update_any(scope_id)
|
||||
};
|
||||
|
||||
// Otherwise, create a new context at the current scope
|
||||
#[allow(unused_mut)]
|
||||
let mut reactive_context = ReactiveContext::new_with_callback(
|
||||
update_scope,
|
||||
scope_id,
|
||||
std::panic::Location::caller(),
|
||||
);
|
||||
#[cfg(debug_assertions)]
|
||||
{
|
||||
// Associate the reactive context with the current scope for debugging
|
||||
reactive_context.inner.write().scope = Some(scope_id);
|
||||
}
|
||||
Some(provide_context(reactive_context))
|
||||
}
|
||||
|
||||
/// Run this function in the context of this reactive context
|
||||
///
|
||||
/// This will set the current reactive context to this context for the duration of the function.
|
||||
/// You can then get information about the current subscriptions.
|
||||
pub fn run_in<O>(&self, f: impl FnOnce() -> O) -> O {
|
||||
CURRENT.with(|current| current.borrow_mut().push(*self));
|
||||
let out = f();
|
||||
CURRENT.with(|current| current.borrow_mut().pop());
|
||||
out
|
||||
}
|
||||
|
||||
/// Marks this reactive context as dirty
|
||||
///
|
||||
/// If there's a scope associated with this context, then it will be marked as dirty too
|
||||
///
|
||||
/// Returns true if the context was marked as dirty, or false if the context has been dropped
|
||||
pub fn mark_dirty(&self) -> bool {
|
||||
if let Ok(mut self_write) = self.inner.try_write_unchecked() {
|
||||
#[cfg(debug_assertions)]
|
||||
{
|
||||
tracing::trace!(
|
||||
"Marking reactive context created at {} as dirty",
|
||||
self_write.origin
|
||||
);
|
||||
}
|
||||
|
||||
(self_write.update)();
|
||||
|
||||
true
|
||||
} else {
|
||||
false
|
||||
}
|
||||
}
|
||||
|
||||
/// Get the scope that inner CopyValue is associated with
|
||||
pub fn origin_scope(&self) -> ScopeId {
|
||||
self.inner.origin_scope()
|
||||
}
|
||||
}
|
||||
|
||||
impl Hash for ReactiveContext {
|
||||
fn hash<H: std::hash::Hasher>(&self, state: &mut H) {
|
||||
self.inner.id().hash(state);
|
||||
}
|
||||
}
|
||||
|
||||
struct Inner {
|
||||
self_: Option<ReactiveContext>,
|
||||
|
||||
// Futures will call .changed().await
|
||||
update: Box<dyn FnMut() + Send + Sync>,
|
||||
|
||||
// Debug information for signal subscriptions
|
||||
#[cfg(debug_assertions)]
|
||||
origin: &'static std::panic::Location<'static>,
|
||||
|
||||
#[cfg(debug_assertions)]
|
||||
// The scope that this reactive context is associated with
|
||||
scope: Option<ScopeId>,
|
||||
}
|
|
@ -1,4 +1,4 @@
|
|||
use crate::{write::Writable, ReactiveContext};
|
||||
use crate::write::Writable;
|
||||
use std::hash::Hash;
|
||||
|
||||
use crate::read::Readable;
|
||||
|
@ -57,7 +57,7 @@ impl<R: Eq + Hash, S: Storage<SignalData<bool>>> SetCompare<R, S> {
|
|||
spawn(async move {
|
||||
loop {
|
||||
// Recompute the value
|
||||
rc.run_in(&mut recompute);
|
||||
rc.reset_and_run_in(&mut recompute);
|
||||
|
||||
// Wait for context to change
|
||||
let _ = changed.next().await;
|
||||
|
|
|
@ -1,9 +1,9 @@
|
|||
use crate::{default_impl, fmt_impls, write_impls};
|
||||
use crate::{read::*, write::*, CopyValue, GlobalMemo, GlobalSignal, ReactiveContext, ReadableRef};
|
||||
use crate::{read::*, write::*, CopyValue, GlobalMemo, GlobalSignal, ReadableRef};
|
||||
use crate::{Memo, WritableRef};
|
||||
use dioxus_core::IntoDynNode;
|
||||
use dioxus_core::{prelude::IntoAttributeValue, ScopeId};
|
||||
use dioxus_core::prelude::*;
|
||||
use generational_box::{AnyStorage, Storage, SyncStorage, UnsyncStorage};
|
||||
use std::sync::Arc;
|
||||
use std::{
|
||||
any::Any,
|
||||
collections::HashSet,
|
||||
|
@ -28,7 +28,7 @@ pub type SyncSignal<T> = Signal<T, SyncStorage>;
|
|||
|
||||
/// The data stored for tracking in a signal.
|
||||
pub struct SignalData<T> {
|
||||
pub(crate) subscribers: Mutex<HashSet<ReactiveContext>>,
|
||||
pub(crate) subscribers: Arc<Mutex<HashSet<ReactiveContext>>>,
|
||||
pub(crate) value: T,
|
||||
}
|
||||
|
||||
|
@ -355,7 +355,7 @@ impl<T, S: Storage<SignalData<T>>> Readable for Signal<T, S> {
|
|||
|
||||
if let Some(reactive_context) = ReactiveContext::current() {
|
||||
tracing::trace!("Subscribing to the reactive context {}", reactive_context);
|
||||
inner.subscribers.lock().unwrap().insert(reactive_context);
|
||||
reactive_context.subscribe(inner.subscribers.clone());
|
||||
}
|
||||
|
||||
Ok(S::map(inner, |v| &v.value))
|
||||
|
|
Loading…
Reference in a new issue