From 3f17a5ffea90b1d9ab7c11a8f019d68f16a1cbdc Mon Sep 17 00:00:00 2001 From: Evan Almloff Date: Tue, 21 Oct 2025 12:50:14 -0500 Subject: [PATCH] Remove write on memo and create a new derived signal hook --- packages/hooks/docs/derived_signal.md | 37 ++++++++++++++++++++++++ packages/hooks/src/lib.rs | 3 ++ packages/hooks/src/use_derived_signal.rs | 14 +++++++++ packages/signals/src/derived_signal.rs | 32 ++++++++++++++++++++ packages/signals/src/lib.rs | 2 ++ packages/signals/src/memo.rs | 18 ++---------- packages/signals/src/signal.rs | 29 +++++++++++++++---- 7 files changed, 113 insertions(+), 22 deletions(-) create mode 100644 packages/hooks/docs/derived_signal.md create mode 100644 packages/hooks/src/use_derived_signal.rs create mode 100644 packages/signals/src/derived_signal.rs diff --git a/packages/hooks/docs/derived_signal.md b/packages/hooks/docs/derived_signal.md new file mode 100644 index 0000000000..c0923c2499 --- /dev/null +++ b/packages/hooks/docs/derived_signal.md @@ -0,0 +1,37 @@ +Creates a new Signal that is derived from other state. The derived signal will automatically update whenever any of the reactive values it reads on are written to. Note the signal is not memorized and the update is not immediate, the signal will be set to the value after the next async tick. Derived signals are useful for initializing values from props. + +```rust +# use dioxus::prelude::*; +# +# fn App() -> Element { +# rsx! { +# Router:: {} +# } +# } +#[derive(Routable, Clone)] +enum Route { + // When you first navigate to this route, initial_count will be used to set the value of + // the count signal + #[route("/:initial_count")] + Counter { initial_count: u32 }, +} + +#[component] +fn Counter(initial_count: ReadSignal) -> Element { + // The count will reset to the value of the prop whenever the prop changes + let mut count = use_derived_signal(move || initial_count()); + + rsx! { + button { + onclick: move |_| count += 1, + "{count}" + } + Link { + // Navigating to this link will change the initial_count prop to 10. Note, this + // only updates the props, the component is not remounted + to: Route::Counter { initial_count: 10 }, + "Go to initial count 10" + } + } +} +``` diff --git a/packages/hooks/src/lib.rs b/packages/hooks/src/lib.rs index 716a53bafc..96e43501cb 100644 --- a/packages/hooks/src/lib.rs +++ b/packages/hooks/src/lib.rs @@ -104,3 +104,6 @@ pub use use_action::*; mod use_waker; pub use use_waker::*; + +mod use_derived_signal; +pub use use_derived_signal::*; diff --git a/packages/hooks/src/use_derived_signal.rs b/packages/hooks/src/use_derived_signal.rs new file mode 100644 index 0000000000..7ca622131f --- /dev/null +++ b/packages/hooks/src/use_derived_signal.rs @@ -0,0 +1,14 @@ +use crate::use_callback; +use dioxus_core::use_hook; +use dioxus_signals::Signal; + +#[doc = include_str!("../docs/derived_signal.md")] +#[doc = include_str!("../docs/rules_of_hooks.md")] +#[doc = include_str!("../docs/moving_state_around.md")] +#[track_caller] +pub fn use_derived_signal(mut f: impl FnMut() -> R + 'static) -> Signal { + let callback = use_callback(move |_| f()); + let caller = std::panic::Location::caller(); + #[allow(clippy::redundant_closure)] + use_hook(|| Signal::derived_signal_with_location(move || callback(()), caller)) +} diff --git a/packages/signals/src/derived_signal.rs b/packages/signals/src/derived_signal.rs new file mode 100644 index 0000000000..973c4751e9 --- /dev/null +++ b/packages/signals/src/derived_signal.rs @@ -0,0 +1,32 @@ +use dioxus_core::{current_scope_id, spawn_isomorphic, ReactiveContext}; +use futures_util::StreamExt; + +use crate::{Signal, WritableExt}; + +pub(crate) fn derived_signal( + mut init: impl FnMut() -> T + 'static, + location: &'static std::panic::Location<'static>, +) -> Signal { + let (tx, mut rx) = futures_channel::mpsc::unbounded(); + + let rc = ReactiveContext::new_with_callback( + move || _ = tx.unbounded_send(()), + current_scope_id(), + location, + ); + + // Create a new signal in that context, wiring up its dependencies and subscribers + let mut recompute = move || rc.reset_and_run_in(&mut init); + let value = recompute(); + let mut state: Signal = Signal::new_with_caller(value, location); + + spawn_isomorphic(async move { + while rx.next().await.is_some() { + // Remove any pending updates + while rx.try_next().is_ok() {} + state.set(recompute()); + } + }); + + state +} diff --git a/packages/signals/src/lib.rs b/packages/signals/src/lib.rs index 926cba75d9..d37df50e3b 100644 --- a/packages/signals/src/lib.rs +++ b/packages/signals/src/lib.rs @@ -44,3 +44,5 @@ pub mod warnings; mod boxed; pub use boxed::*; + +mod derived_signal; diff --git a/packages/signals/src/memo.rs b/packages/signals/src/memo.rs index a937744728..8f2c588e81 100644 --- a/packages/signals/src/memo.rs +++ b/packages/signals/src/memo.rs @@ -1,6 +1,6 @@ -use crate::{read::Readable, write_impls, ReadableRef, Signal}; +use crate::CopyValue; +use crate::{read::Readable, ReadableRef, Signal}; use crate::{read_impls, GlobalMemo, ReadableExt, WritableExt}; -use crate::{CopyValue, Writable}; use std::{ cell::RefCell, ops::Deref, @@ -214,19 +214,6 @@ where } } -impl Writable for Memo { - type WriteMetadata = as Writable>::WriteMetadata; - - fn try_write_unchecked( - &self, - ) -> Result, generational_box::BorrowMutError> - where - Self::Target: 'static, - { - self.inner.try_write_unchecked() - } -} - impl IntoAttributeValue for Memo where T: Clone + IntoAttributeValue + PartialEq + 'static, @@ -263,7 +250,6 @@ where } read_impls!(Memo where T: PartialEq); -write_impls!(Memo where T: PartialEq); impl Clone for Memo { fn clone(&self) -> Self { diff --git a/packages/signals/src/signal.rs b/packages/signals/src/signal.rs index 1e51a5ae5c..c05886fe8f 100644 --- a/packages/signals/src/signal.rs +++ b/packages/signals/src/signal.rs @@ -1,6 +1,6 @@ use crate::{ - default_impl, fmt_impls, read::*, write::*, write_impls, CopyValue, Global, GlobalMemo, - GlobalSignal, Memo, ReadableRef, WritableRef, + default_impl, derived_signal::derived_signal, fmt_impls, read::*, write::*, write_impls, + CopyValue, Global, GlobalMemo, GlobalSignal, Memo, ReadableRef, WritableRef, }; use dioxus_core::{IntoAttributeValue, IntoDynNode, ReactiveContext, ScopeId, Subscribers}; use generational_box::{BorrowResult, Storage, SyncStorage, UnsyncStorage}; @@ -120,17 +120,17 @@ impl Signal { GlobalMemo::new(constructor) } - /// Creates a new unsync Selector. The selector will be run immediately and whenever any signal it reads changes. + /// Creates a new unsync Memo. The memo will be run immediately and whenever any signal it reads changes. /// - /// Selectors can be used to efficiently compute derived data from signals. + /// Memos can be used to efficiently compute derived data from signals. #[track_caller] pub fn memo(f: impl FnMut() -> T + 'static) -> Memo { Memo::new(f) } - /// Creates a new unsync Selector with an explicit location. The selector will be run immediately and whenever any signal it reads changes. + /// Creates a new unsync Memo with an explicit location. The memo will be run immediately and whenever any signal it reads changes. /// - /// Selectors can be used to efficiently compute derived data from signals. + /// Memos can be used to efficiently compute derived data from signals. pub fn memo_with_location( f: impl FnMut() -> T + 'static, location: &'static std::panic::Location<'static>, @@ -139,6 +139,23 @@ impl Signal { } } +impl Signal { + /// Creates a new derived Signal. The signal will contain the result of the provided function and will automatically update after the + /// next async tick whenever any signal read during the function changes. + pub fn derived_signal(f: impl FnMut() -> T + 'static) -> Self { + derived_signal(f, std::panic::Location::caller()) + } + + /// Creates a new derived Signal with an explicit location. The signal will contain the result of the provided function and will automatically update after the + /// next async tick whenever any signal read during the function changes. + pub fn derived_signal_with_location( + f: impl FnMut() -> T + 'static, + location: &'static std::panic::Location<'static>, + ) -> Self { + derived_signal(f, location) + } +} + impl>> Signal { /// Creates a new Signal. Signals are a Copy state management solution with automatic dependency tracking. #[track_caller]