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:
1 parent
24d247dc85
commit
b6dc2a2230
18 files changed
+459
-250
No files matched your search
@@ -1,112 +0,0 @@
|
||||
# Reactivity
|
||||
|
||||
The core of dioxus relies on the concept of reactivity. Reactivity is the system that updates your app when state changes.
|
||||
|
||||
There are two parts to reactivity: **Reactive Contexts** and **Tracked Values**.
|
||||
|
||||
## Reactive Contexts
|
||||
|
||||
Reactive Contexts keep track of what state different parts of your app rely on. Reactive Context show up throughout dioxus: Component, use_effect, use_memo and use_resource all have their own reactive contexts:
|
||||
|
||||
```rust, no_run
|
||||
# use dioxus::prelude::*;
|
||||
let count = use_signal(|| 0);
|
||||
// The reactive context in the memo knows that the memo depends on the count signal
|
||||
use_memo(move || count() * 2);
|
||||
```
|
||||
|
||||
## Tracked Values
|
||||
|
||||
Tracked values are values that reactive contexts know about. When you read a tracked value, the reactive context will rerun when the value changes. Signals, Memos, and Resources are all tracked values:
|
||||
|
||||
```rust, no_run
|
||||
# use dioxus::prelude::*;
|
||||
// The count signal is tracked
|
||||
let count = use_signal(|| 0);
|
||||
// When you read the count signal, the reactive context subscribes to the count signal
|
||||
let double_count = use_memo(move || count() * 2);
|
||||
```
|
||||
|
||||
## Reactivity
|
||||
|
||||
Reactivity is the system that combines reactive context and tracked values to update your app when state changes.
|
||||
|
||||
You can use reactivity to create derived state and update your app when state changes.
|
||||
|
||||
You can derive state from other state with [`use_memo`](https://docs.rs/dioxus/latest/dioxus/prelude/fn.use_memo.html).
|
||||
|
||||
```rust, no_run
|
||||
use dioxus::prelude::*;
|
||||
|
||||
let mut count = use_signal(|| 0);
|
||||
let double_count = use_memo(move || count() * 2);
|
||||
|
||||
// Now whenever we read double_count, we know it is always twice the value of count
|
||||
println!("{}", double_count); // Prints "2"
|
||||
|
||||
// After we write to count, the reactive context will rerun and double_count will be updated automatically
|
||||
count += 1;
|
||||
|
||||
println!("{}", double_count); // Prints "4"
|
||||
```
|
||||
|
||||
You can also use reactivity to create derive state asynchronously. For example, you can use [`use_resource`](https://docs.rs/dioxus/latest/dioxus/prelude/fn.use_resource.html) to load data from a server:
|
||||
|
||||
```rust, no_run
|
||||
use dioxus::prelude::*;
|
||||
|
||||
let count = use_signal(|| 0);
|
||||
let double_count = use_resource(move || async move {
|
||||
// Start a request to the server. We are reading the value of count to format it into the url
|
||||
// Since we are reading count, this resource will "subscribe" to changes to count (when count changes, the resource will rerun)
|
||||
let response = reqwest::get(format!("https://myserver.com/doubleme?count={count}")).await.unwrap();
|
||||
response.text().await.unwrap()
|
||||
});
|
||||
```
|
||||
|
||||
## Non Reactive State
|
||||
|
||||
You can use plain Rust types in Dioxus, but you should be aware that they are not reactive. If you read the non-reactive state, reactive scopes will not subscribe to the state.
|
||||
|
||||
You can make non-reactive state reactive by using the `Signal` type instead of a plain Rust type or by using the `use_reactive` hook.
|
||||
|
||||
```rust, no_run
|
||||
use dioxus::prelude::*;
|
||||
|
||||
// ❌ Don't create non-reactive state
|
||||
let state = use_hook(|| std::cell::RefCell::new(0));
|
||||
|
||||
// Computed values will get out of date if the state they depend on is not reactive
|
||||
let doubled = use_memo(move || *state.borrow() * 2);
|
||||
|
||||
// ✅ Create reactive state
|
||||
let state = use_signal(|| 0);
|
||||
|
||||
// Computed values will automatically keep up to date with the latest reactive state
|
||||
let doubled = use_memo(move || state() * 2);
|
||||
|
||||
// ❌ Don't depend on non-reactive prop state in memos/resources
|
||||
#[component]
|
||||
fn MyComponent(state: i32) -> Element {
|
||||
let doubled = use_memo(move || state * 2);
|
||||
todo!()
|
||||
}
|
||||
|
||||
// ✅ Wrap your props in ReadOnlySignal to make them reactive
|
||||
#[component]
|
||||
fn MyReactiveComponent(state: ReadOnlySignal<i32>) -> Element {
|
||||
let doubled = use_memo(move || state() * 2);
|
||||
todo!()
|
||||
}
|
||||
```
|
||||
|
||||
If your state can't be reactive, you can use the `use_reactive` hook to make it reactive.
|
||||
|
||||
```rust, no_run
|
||||
use dioxus::prelude::*;
|
||||
|
||||
let state = rand::random::<i32>();
|
||||
|
||||
// You can make the state reactive by wrapping it in use_reactive
|
||||
let doubled = use_memo(use_reactive!(|state| state * 2));
|
||||
```
|
||||
@@ -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))
|
||||
|
||||
Reference in new issue
Block a user