diff --git a/packages/hooks/src/lib.rs b/packages/hooks/src/lib.rs index a179e22ee..3af9dddda 100644 --- a/packages/hooks/src/lib.rs +++ b/packages/hooks/src/lib.rs @@ -53,8 +53,6 @@ macro_rules! to_owned { $(to_owned![$($rest)*])? }; } -mod dependency; -pub use dependency::*; mod use_callback; pub use use_callback::*; @@ -71,6 +69,9 @@ pub use use_coroutine::*; mod use_future; pub use use_future::*; +mod use_reactive; +pub use use_reactive::*; + // mod use_sorted; // pub use use_sorted::*; diff --git a/packages/hooks/src/use_effect.rs b/packages/hooks/src/use_effect.rs index 55b0d332e..5a7792756 100644 --- a/packages/hooks/src/use_effect.rs +++ b/packages/hooks/src/use_effect.rs @@ -2,6 +2,8 @@ use dioxus_core::prelude::*; use dioxus_signals::ReactiveContext; use futures_util::StreamExt; +use crate::use_callback; + /// `use_effect` will subscribe to any changes in the signal values it captures /// effects will always run after first mount and then whenever the signal values change /// If the use_effect call was skipped due to an early return, the effect will no longer activate. @@ -19,19 +21,39 @@ use futures_util::StreamExt; /// } /// } /// ``` +/// +/// ## With non-reactive dependencies +/// To add non-reactive dependencies, you can use the `use_reactive` hook. +/// +/// Signals will automatically be added as dependencies, so you don't need to call this method for them. +/// +/// ```rust +/// # use dioxus::prelude::*; +/// # async fn sleep(delay: u32) {} +/// +/// #[component] +/// fn Comp(count: u32) -> Element { +/// // Because the memo subscribes to `count` by adding it as a dependency, the memo will rerun every time `count` changes. +/// use_effect(use_reactive((&count, |(count,)| println!("Manually manipulate the dom") ))); +/// +/// todo!() +/// } +/// ``` #[track_caller] -pub fn use_effect(mut callback: impl FnMut() + 'static) { +pub fn use_effect(callback: impl FnMut() + 'static) -> Effect { // let mut run_effect = use_hook(|| CopyValue::new(true)); // use_hook_did_run(move |did_run| run_effect.set(did_run)); + let callback = use_callback(callback); + let location = std::panic::Location::caller(); use_hook(|| { + let (rc, mut changed) = ReactiveContext::new_with_origin(location); spawn(async move { - let (rc, mut changed) = ReactiveContext::new_with_origin(location); loop { // Run the effect - rc.run_in(&mut callback); + rc.run_in(&*callback); // Wait for context to change let _ = changed.next().await; @@ -40,5 +62,19 @@ pub fn use_effect(mut callback: impl FnMut() + 'static) { wait_for_next_render().await; } }); - }); + Effect { rc } + }) +} + +/// A handle to an effect. +#[derive(Clone, Copy)] +pub struct Effect { + rc: ReactiveContext, +} + +impl Effect { + /// Marks the effect as dirty, causing it to rerun on the next render. + pub fn mark_dirty(&mut self) { + self.rc.mark_dirty(); + } } diff --git a/packages/hooks/src/use_memo.rs b/packages/hooks/src/use_memo.rs index 2f423e07a..bd5b98171 100644 --- a/packages/hooks/src/use_memo.rs +++ b/packages/hooks/src/use_memo.rs @@ -1,14 +1,10 @@ -use crate::dependency::Dependency; -use crate::{use_callback, use_signal}; +use crate::use_callback; use dioxus_core::prelude::*; -use dioxus_signals::Memo; -use dioxus_signals::{ReactiveContext, ReadOnlySignal, Readable, Signal, SignalData}; -use dioxus_signals::{Storage, Writable}; -use futures_util::StreamExt; +use dioxus_signals::{Memo, Signal}; -/// Creates a new unsync Selector. The selector will be run immediately and whenever any signal it reads changes. +/// Creates a new 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. /// /// ```rust /// use dioxus::prelude::*; @@ -23,98 +19,27 @@ use futures_util::StreamExt; /// rsx! { "{double}" } /// } /// ``` +/// +/// ## With non-reactive dependencies +/// To add non-reactive dependencies, you can use the `use_reactive` hook. +/// +/// Signals will automatically be added as dependencies, so you don't need to call this method for them. +/// +/// ```rust +/// # use dioxus::prelude::*; +/// # async fn sleep(delay: u32) {} +/// +/// #[component] +/// fn Comp(count: u32) -> Element { +/// // Because the memo subscribes to `count` by adding it as a dependency, the memo will rerun every time `count` changes. +/// let new_count = use_memo(use_reactive((&count, |(count,)| count + 1))); +/// +/// todo!() +/// } +/// ``` #[track_caller] pub fn use_memo(f: impl FnMut() -> R + 'static) -> Memo { let callback = use_callback(f); #[allow(clippy::redundant_closure)] use_hook(|| Signal::memo(move || callback())) } - -/// Creates a new unsync Selector with some local dependencies. The selector will be run immediately and whenever any signal it reads or any dependencies it tracks changes -/// -/// Selectors can be used to efficiently compute derived data from signals. -/// -/// ```rust -/// use dioxus::prelude::*; -/// -/// fn App() -> Element { -/// let mut local_state = use_signal(|| 0); -/// let double = use_memo_with_dependencies((&local_state(),), move |(local_state,)| local_state * 2); -/// local_state.set(1); -/// -/// rsx! { "{double}" } -/// } -/// ``` -#[track_caller] -pub fn use_memo_with_dependencies( - dependencies: D, - f: impl FnMut(D::Out) -> R + 'static, -) -> ReadOnlySignal -where - D::Out: 'static, -{ - use_maybe_sync_memo_with_dependencies(dependencies, f) -} - -/// Creates a new Selector that may be sync with some local dependencies. The selector will be run immediately and whenever any signal it reads or any dependencies it tracks changes -/// -/// Selectors can be used to efficiently compute derived data from signals. -/// -/// ```rust -/// use dioxus::prelude::*; -/// use dioxus_signals::*; -/// -/// fn App() -> Element { -/// let mut local_state = use_signal(|| 0i32); -/// let double: ReadOnlySignal = use_maybe_sync_memo_with_dependencies((&local_state(),), move |(local_state,)| local_state * 2); -/// local_state.set(1); -/// -/// rsx! { "{double}" } -/// } -/// ``` -#[track_caller] -pub fn use_maybe_sync_memo_with_dependencies< - R: PartialEq, - D: Dependency, - S: Storage>, ->( - dependencies: D, - mut f: impl FnMut(D::Out) -> R + 'static, -) -> ReadOnlySignal -where - D::Out: 'static, -{ - let mut dependencies_signal = use_signal(|| dependencies.out()); - - let selector = use_hook(|| { - // Get the current reactive context - let (rc, mut changed) = ReactiveContext::new(); - - // Create a new signal in that context, wiring up its dependencies and subscribers - let mut state: Signal = - rc.run_in(|| Signal::new_maybe_sync(f(dependencies_signal.read().clone()))); - - spawn(async move { - loop { - // Wait for context to change - let _ = changed.next().await; - - let new = rc.run_in(|| f(dependencies_signal.read().clone())); - if new != *state.peek() { - *state.write() = new; - } - } - }); - - // And just return the readonly variant of that signal - ReadOnlySignal::new_maybe_sync(state) - }); - - // This will cause a re-run of the selector if the dependencies change - let changed = { dependencies.changed(&*dependencies_signal.read()) }; - if changed { - dependencies_signal.set(dependencies.out()); - } - - selector -} diff --git a/packages/hooks/src/dependency.rs b/packages/hooks/src/use_reactive.rs similarity index 50% rename from packages/hooks/src/dependency.rs rename to packages/hooks/src/use_reactive.rs index d6ab65a5f..96a7289e4 100644 --- a/packages/hooks/src/dependency.rs +++ b/packages/hooks/src/use_reactive.rs @@ -1,7 +1,11 @@ +use dioxus_signals::{Readable, Writable}; + +use crate::use_signal; + /// A dependency is a trait that can be used to determine if a effect or selector should be re-run. pub trait Dependency: Sized + Clone { /// The output of the dependency - type Out: Clone + PartialEq; + type Out: Clone + PartialEq + 'static; /// Returns the output of the dependency. fn out(&self) -> Self::Out; /// Returns true if the dependency has changed. @@ -16,10 +20,10 @@ impl Dependency for () { } /// A dependency is a trait that can be used to determine if a effect or selector should be re-run. -pub trait Dep: 'static + PartialEq + Clone {} -impl Dep for T where T: 'static + PartialEq + Clone {} +pub trait DependencyElement: 'static + PartialEq + Clone {} +impl DependencyElement for T where T: 'static + PartialEq + Clone {} -impl Dependency for &A { +impl Dependency for &A { type Out = A; fn out(&self) -> Self::Out { (*self).clone() @@ -33,7 +37,7 @@ macro_rules! impl_dep { impl< $($el),* > Dependency for ($(&$el,)*) where $( - $el: Dep + $el: DependencyElement ),* { type Out = ($($el,)*); @@ -65,3 +69,57 @@ impl_dep!(A = a1 a2, B = b1 b2, C = c1 c2, D = d1 d2, E = e1 e2,); impl_dep!(A = a1 a2, B = b1 b2, C = c1 c2, D = d1 d2, E = e1 e2, F = f1 f2,); impl_dep!(A = a1 a2, B = b1 b2, C = c1 c2, D = d1 d2, E = e1 e2, F = f1 f2, G = g1 g2,); impl_dep!(A = a1 a2, B = b1 b2, C = c1 c2, D = d1 d2, E = e1 e2, F = f1 f2, G = g1 g2, H = h1 h2,); + +/// Takes some non-reactive data, and a closure and returns a closure that will subscribe to that non-reactive data as if it were reactive. +/// +/// # Example +/// +/// ```rust +/// use dioxus::prelude::*; +/// +/// let data = 5; +/// +/// use_effect(use_reactive((&data,), |(data,)| { +/// println!("Data changed: {}", data); +/// })); +/// ``` +pub fn use_reactive( + non_reactive_data: D, + mut closure: impl FnMut(D::Out) -> O + 'static, +) -> impl FnMut() -> O + 'static { + let mut first_run = false; + let mut last_state = use_signal(|| { + first_run = true; + non_reactive_data.out() + }); + if !first_run && non_reactive_data.changed(&*last_state.peek()) { + last_state.set(non_reactive_data.out()); + } + move || closure(last_state()) +} + +/// A helper macro for `use_reactive` that merges uses the closure syntax to elaborate the dependency array +/// +/// Takes some non-reactive data, and a closure and returns a closure that will subscribe to that non-reactive data as if it were reactive. +/// +/// # Example +/// +/// ```rust +/// use dioxus::prelude::*; +/// +/// let data = 5; +/// +/// use_effect(use_reactive!(|data| { +/// println!("Data changed: {}", data); +/// })); +/// ``` +#[macro_export] +macro_rules! use_reactive { + (|| $($rest:tt)*) => { use_reactive( (), move |_| $($rest)* ) }; + (| $($args:tt),* | $($rest:tt)*) => { + use_reactive( + ($(&$args),*), + move |($($args),*)| $($rest)* + ) + }; +} diff --git a/packages/hooks/src/use_resource.rs b/packages/hooks/src/use_resource.rs index f10db5e38..3af0a0a30 100644 --- a/packages/hooks/src/use_resource.rs +++ b/packages/hooks/src/use_resource.rs @@ -35,7 +35,7 @@ use std::{cell::Cell, future::Future, rc::Rc}; /// }); /// /// // Because the resource's future subscribes to `country` by reading it (`country.read()`), -/// // everytime `country` changes the resource's future will run again and thus provide a new value. +/// // every time `country` changes the resource's future will run again and thus provide a new value. /// let current_weather = use_resource(move || async move { get_weather(&country()).await }); /// /// rsx! { @@ -51,8 +51,26 @@ use std::{cell::Cell, future::Future, rc::Rc}; /// } ///} /// ``` +/// +/// ## With non-reactive dependencies +/// To add non-reactive dependencies, you can use the `use_reactive` hook. +/// +/// Signals will automatically be added as dependencies, so you don't need to call this method for them. +/// +/// ```rust +/// # use dioxus::prelude::*; +/// # async fn sleep(delay: u32) {} +/// +/// #[component] +/// fn Comp(count: u32) -> Element { +/// // Because the memo subscribes to `count` by adding it as a dependency, the memo will rerun every time `count` changes. +/// let new_count = use_resource(use_reactive((&count, |(count,)| async move {count + 1} ))); +/// +/// todo!() +/// } +/// ``` #[must_use = "Consider using `cx.spawn` to run a future without reading its value"] -pub fn use_resource(future: impl Fn() -> F + 'static) -> Resource +pub fn use_resource(mut future: impl FnMut() -> F + 'static) -> Resource where T: 'static, F: Future + 'static, @@ -66,10 +84,9 @@ where let cb = use_callback(move || { // Create the user's task - #[allow(clippy::redundant_closure)] - let fut = rc.run_in(|| future()); + let fut = rc.run_in(&mut future); - // Spawn a wrapper task that polls the innner future and watch its dependencies + // Spawn a wrapper task that polls the inner future and watch its dependencies spawn(async move { // move the future here and pin it so we can poll it let fut = fut; diff --git a/packages/signals/examples/dependancies.rs b/packages/signals/examples/dependancies.rs index 34159d34a..70319da40 100644 --- a/packages/signals/examples/dependancies.rs +++ b/packages/signals/examples/dependancies.rs @@ -14,16 +14,39 @@ fn app() -> Element { } }); - let mut local_state = use_signal(|| 0); - - let computed = use_memo_with_dependencies((&local_state(),), move |(local_state,)| { - local_state * 2 + signal.cloned() - }); - - println!("Running app"); - rsx! { - button { onclick: move |_| local_state.set(local_state() + 1), "Add one" } - div { "{computed}" } + "Parent count: {signal}" + Child { + non_reactive_prop: signal() + } + } +} + +#[component] +fn Child(non_reactive_prop: i32) -> Element { + let mut signal = use_signal(|| 0); + + // You can manually specify the dependencies with `use_reactive` for values that are not reactive like props + let computed = use_memo(use_reactive!( + |(non_reactive_prop,)| non_reactive_prop + signal() + )); + use_effect(use_reactive!(|(non_reactive_prop,)| println!( + "{}", + non_reactive_prop + signal() + ))); + let fut = use_resource(use_reactive!(|(non_reactive_prop,)| async move { + tokio::time::sleep(std::time::Duration::from_secs(1)).await; + non_reactive_prop + signal() + })); + + rsx! { + button { + onclick: move |_| signal += 1, + "Child count: {signal}" + } + + "Sum: {computed}" + + "{fut():?}" } }