From 974680796f01b8985882e18d39b1b2d74f4a5069 Mon Sep 17 00:00:00 2001 From: Jonathan Kelley Date: Fri, 26 Jan 2024 23:05:40 -0800 Subject: [PATCH] add test cases, refactor deref --- examples/backgrounded_futures.rs | 55 ++++++++++++++++++++++++ examples/memo_chain.rs | 2 + examples/stale_memo.rs | 36 ++++++++++++++++ packages/hooks/src/use_future.rs | 8 +++- packages/signals/src/read.rs | 36 +++++++++++++++- packages/signals/src/read_only_signal.rs | 27 +----------- packages/signals/src/signal.rs | 27 +----------- 7 files changed, 137 insertions(+), 54 deletions(-) create mode 100644 examples/backgrounded_futures.rs create mode 100644 examples/stale_memo.rs diff --git a/examples/backgrounded_futures.rs b/examples/backgrounded_futures.rs new file mode 100644 index 000000000..2877daab3 --- /dev/null +++ b/examples/backgrounded_futures.rs @@ -0,0 +1,55 @@ +use dioxus::prelude::*; + +fn main() { + launch_desktop(app); +} + +fn app() -> Element { + let mut show_child = use_signal(|| true); + let mut count = use_signal(|| 0); + + let child = use_memo(move || { + rsx! { + Child { + count + } + } + }); + + rsx! { + button { onclick: move |_| show_child.toggle(), "Toggle child" } + button { onclick: move |_| count += 1, "Increment count" } + if show_child() { + {child.cloned()} + } + } +} + +#[component] +fn Child(count: Signal) -> Element { + let mut early_return = use_signal(|| false); + + let early = rsx! { + button { onclick: move |_| early_return.toggle(), "Toggle {early_return} early return" } + }; + + if early_return() { + return early; + } + + use_future(move || async move { + loop { + tokio::time::sleep(std::time::Duration::from_millis(100)).await; + println!("Child") + } + }); + + use_effect(move || { + println!("Child count: {}", count()); + }); + + rsx! { + "hellO!" + {early} + } +} diff --git a/examples/memo_chain.rs b/examples/memo_chain.rs index 3d9c8ae30..3fd80d1b9 100644 --- a/examples/memo_chain.rs +++ b/examples/memo_chain.rs @@ -37,6 +37,8 @@ fn Child( println!("rendering child: {}", depth()); + // These memos don't get re-computed when early returns happen + // In dioxus futures spawned with use_future won't progress if they don't get hit during rendering let state = use_memo(move || state() + 1); let item = use_memo(move || items()[dbg!(depth()) - 1]); let depth = use_memo(move || depth() - 1); diff --git a/examples/stale_memo.rs b/examples/stale_memo.rs new file mode 100644 index 000000000..10f7bcebe --- /dev/null +++ b/examples/stale_memo.rs @@ -0,0 +1,36 @@ +use dioxus::prelude::*; + +fn main() { + launch_desktop(app); +} + +fn app() -> Element { + let mut state = use_signal(|| 0); + let mut depth = use_signal(|| 1 as usize); + + if depth() == 5 { + return rsx! { + div { "Max depth reached" } + button { onclick: move |_| depth -= 1, "Remove depth" } + }; + } + + let mut items = use_memo(move || (0..depth()).map(|f| f as _).collect::>()); + + rsx! { + button { onclick: move |_| state += 1, "Increment" } + button { onclick: move |_| depth += 1, "Add depth" } + button { + onclick: move |_| async move { + depth += 1; + tokio::time::sleep(std::time::Duration::from_millis(100)).await; + dbg!(items.read()); + // if depth() is 5, this will be the old since the memo hasn't been re-computed + // use_memos are only re-computed when the signals they capture change + // *and* they are used in the current render + // If the use_memo isn't used, it can't be re-computed! + }, + "Add depth with sleep" + } + } +} diff --git a/packages/hooks/src/use_future.rs b/packages/hooks/src/use_future.rs index 1a95c03f3..5a7d955b3 100644 --- a/packages/hooks/src/use_future.rs +++ b/packages/hooks/src/use_future.rs @@ -1,6 +1,6 @@ #![allow(missing_docs)] use dioxus_core::{ - prelude::{spawn, use_hook}, + prelude::{spawn, use_drop, use_hook}, ScopeState, Task, }; use dioxus_signals::*; @@ -46,6 +46,12 @@ where Some(task) }); + use_drop(move || { + if let Some(task) = task.take() { + task.stop(); + } + }); + UseFuture { task, state } } diff --git a/packages/signals/src/read.rs b/packages/signals/src/read.rs index fa308210d..ec3263072 100644 --- a/packages/signals/src/read.rs +++ b/packages/signals/src/read.rs @@ -1,4 +1,4 @@ -use std::ops::Deref; +use std::{mem::MaybeUninit, ops::Deref}; /// A trait for states that can be read from like [`crate::Signal`], [`crate::GlobalSignal`], or [`crate::ReadOnlySignal`]. You may choose to accept this trait as a parameter instead of the concrete type to allow for more flexibility in your API. For example, instead of creating two functions, one that accepts a [`crate::Signal`] and one that accepts a [`crate::GlobalSignal`], you can create one function that accepts a [`Readable`] type. pub trait Readable { @@ -49,6 +49,40 @@ pub trait Readable { { Self::map_ref(self.read(), |v| v.index(index)) } + + #[doc(hidden)] + fn deref_impl<'a>(&self) -> &'a dyn Fn() -> T + where + Self: Sized + 'a, + T: Clone, + { + // https://github.com/dtolnay/case-studies/tree/master/callable-types + + // First we create a closure that captures something with the Same in memory layout as Self (MaybeUninit). + let uninit_callable = MaybeUninit::::uninit(); + // Then move that value into the closure. We assume that the closure now has a in memory layout of Self. + let uninit_closure = move || Self::read(unsafe { &*uninit_callable.as_ptr() }).clone(); + + // Check that the size of the closure is the same as the size of Self in case the compiler changed the layout of the closure. + let size_of_closure = std::mem::size_of_val(&uninit_closure); + assert_eq!(size_of_closure, std::mem::size_of::()); + + // Then cast the lifetime of the closure to the lifetime of &self. + fn cast_lifetime<'a, T>(_a: &T, b: &'a T) -> &'a T { + b + } + let reference_to_closure = cast_lifetime( + { + // The real closure that we will never use. + &uninit_closure + }, + // We transmute self into a reference to the closure. This is safe because we know that the closure has the same memory layout as Self so &Closure == &Self. + unsafe { std::mem::transmute(self) }, + ); + + // Cast the closure to a trait object. + reference_to_closure as &_ + } } /// An extension trait for Readable> that provides some convenience methods. diff --git a/packages/signals/src/read_only_signal.rs b/packages/signals/src/read_only_signal.rs index 5103cedcf..d9cea83aa 100644 --- a/packages/signals/src/read_only_signal.rs +++ b/packages/signals/src/read_only_signal.rs @@ -110,31 +110,6 @@ impl> + 'static> Deref for ReadOnlySignal T; fn deref(&self) -> &Self::Target { - // https://github.com/dtolnay/case-studies/tree/master/callable-types - - // First we create a closure that captures something with the Same in memory layout as Self (MaybeUninit). - let uninit_callable = MaybeUninit::::uninit(); - // Then move that value into the closure. We assume that the closure now has a in memory layout of Self. - let uninit_closure = move || Self::read(unsafe { &*uninit_callable.as_ptr() }).clone(); - - // Check that the size of the closure is the same as the size of Self in case the compiler changed the layout of the closure. - let size_of_closure = std::mem::size_of_val(&uninit_closure); - assert_eq!(size_of_closure, std::mem::size_of::()); - - // Then cast the lifetime of the closure to the lifetime of &self. - fn cast_lifetime<'a, T>(_a: &T, b: &'a T) -> &'a T { - b - } - let reference_to_closure = cast_lifetime( - { - // The real closure that we will never use. - &uninit_closure - }, - // We transmute self into a reference to the closure. This is safe because we know that the closure has the same memory layout as Self so &Closure == &Self. - unsafe { std::mem::transmute(self) }, - ); - - // Cast the closure to a trait object. - reference_to_closure as &Self::Target + Readable::deref_impl(self) } } diff --git a/packages/signals/src/signal.rs b/packages/signals/src/signal.rs index a11df5640..1fdef1a1f 100644 --- a/packages/signals/src/signal.rs +++ b/packages/signals/src/signal.rs @@ -501,32 +501,7 @@ impl> + 'static> Deref for Signal { type Target = dyn Fn() -> T; fn deref(&self) -> &Self::Target { - // https://github.com/dtolnay/case-studies/tree/master/callable-types - - // First we create a closure that captures something with the Same in memory layout as Self (MaybeUninit). - let uninit_callable = MaybeUninit::::uninit(); - // Then move that value into the closure. We assume that the closure now has a in memory layout of Self. - let uninit_closure = move || Self::read(unsafe { &*uninit_callable.as_ptr() }).clone(); - - // Check that the size of the closure is the same as the size of Self in case the compiler changed the layout of the closure. - let size_of_closure = std::mem::size_of_val(&uninit_closure); - assert_eq!(size_of_closure, std::mem::size_of::()); - - // Then cast the lifetime of the closure to the lifetime of &self. - fn cast_lifetime<'a, T>(_a: &T, b: &'a T) -> &'a T { - b - } - let reference_to_closure = cast_lifetime( - { - // The real closure that we will never use. - &uninit_closure - }, - // We transmute self into a reference to the closure. This is safe because we know that the closure has the same memory layout as Self so &Closure == &Self. - unsafe { std::mem::transmute(self) }, - ); - - // Cast the closure to a trait object. - reference_to_closure as &Self::Target + Readable::deref_impl(self) } }