diff --git a/examples/memo_chain.rs b/examples/memo_chain.rs index 3fd80d1b9..68f0f2de1 100644 --- a/examples/memo_chain.rs +++ b/examples/memo_chain.rs @@ -6,7 +6,7 @@ fn main() { fn app() -> Element { let mut state = use_signal(|| 0); - let mut depth = use_signal(|| 1 as usize); + let mut depth = use_signal(|| 0 as usize); let mut items = use_memo(move || (0..depth()).map(|f| f as _).collect::>()); let a = use_memo(move || state() + 1); @@ -35,14 +35,14 @@ fn Child( return None; } - 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 item = use_memo(move || items()[depth()]); let depth = use_memo(move || depth() - 1); + println!("rendering child: {}", depth()); + rsx! { h3 { "Depth({depth})-Item({item}): {state}"} Child { diff --git a/packages/core/src/global_context.rs b/packages/core/src/global_context.rs index 52c18e8cb..801eae6ab 100644 --- a/packages/core/src/global_context.rs +++ b/packages/core/src/global_context.rs @@ -71,7 +71,7 @@ pub fn spawn_forever(fut: impl Future + 'static) -> Option { /// /// This drops the task immediately. pub fn remove_future(id: Task) { - Runtime::with_current_scope(|cx| cx.remove_future(id)); + Runtime::with(|rt| rt.remove_task(id)).expect("Runtime to exist"); } /// Store a value between renders. The foundational hook for all other hooks. @@ -94,7 +94,6 @@ pub fn use_hook(initializer: impl FnOnce() -> State) -> Runtime::with_current_scope(|cx| cx.use_hook(initializer)).expect("to be in a dioxus runtime") } - /// Get the current render since the inception of this component /// /// This can be used as a helpful diagnostic when debugging hooks/renders, etc @@ -217,7 +216,6 @@ pub fn use_drop(destroy: D) { }); } - /// Push a function to be run before the next render /// This is a hook and will always run, so you can't unschedule it /// Will run for every progression of suspense, though this might change in the future diff --git a/packages/core/src/scope_context.rs b/packages/core/src/scope_context.rs index b76803ed9..fe6455807 100644 --- a/packages/core/src/scope_context.rs +++ b/packages/core/src/scope_context.rs @@ -234,13 +234,6 @@ impl Scope { Runtime::with(|rt| rt.spawn(self.id, fut)).expect("Runtime to exist") } - /// Informs the scheduler that this task is no longer needed and should be removed. - /// - /// This drops the task immediately. - pub fn remove_future(&self, id: Task) { - Runtime::with(|rt| rt.remove_task(id)).expect("Runtime to exist"); - } - /// Mark this component as suspended and then return None pub fn suspend(&self) -> Option { self.suspended.set(true); diff --git a/packages/core/src/tasks.rs b/packages/core/src/tasks.rs index db6ba7d30..1d8d680e6 100644 --- a/packages/core/src/tasks.rs +++ b/packages/core/src/tasks.rs @@ -45,12 +45,19 @@ impl Task { Runtime::with(|rt| !rt.tasks.borrow()[self.0].active.get()).unwrap_or_default() } + pub fn wake(&self) { + Runtime::with(|rt| _ = rt.sender.unbounded_send(SchedulerMsg::TaskNotified(*self))); + } + + pub fn set_active(&self, active: bool) { + Runtime::with(|rt| rt.tasks.borrow()[self.0].active.set(active)); + } + /// Resume the task. pub fn resume(&self) { Runtime::with(|rt| { // set the active flag, and then ping the scheduler to ensure the task gets queued - rt.tasks.borrow()[self.0].active.set(true); - _ = rt.sender.unbounded_send(SchedulerMsg::TaskNotified(*self)); + let was_active = rt.tasks.borrow()[self.0].active.replace(true); }); } } diff --git a/packages/hooks/src/lib.rs b/packages/hooks/src/lib.rs index 8cd70f70a..d89358f18 100644 --- a/packages/hooks/src/lib.rs +++ b/packages/hooks/src/lib.rs @@ -55,6 +55,9 @@ macro_rules! to_owned { }; } +mod use_callback; +pub use use_callback::*; + mod use_on_destroy; pub use use_on_destroy::*; diff --git a/packages/hooks/src/use_callback.rs b/packages/hooks/src/use_callback.rs new file mode 100644 index 000000000..beee1f2f6 --- /dev/null +++ b/packages/hooks/src/use_callback.rs @@ -0,0 +1,48 @@ +use dioxus_core::prelude::use_hook; +use dioxus_signals::CopyValue; +use dioxus_signals::Writable; + +/// A callback that's always current +/// +/// Whenever this hook is called the inner callback will be replaced with the new callback but the handle will remain. +/// +/// There is *currently* no signal tracking on the Callback so anything reading from it will not be updated. +/// +/// This API is in flux and might not remain. +pub fn use_callback(f: impl FnMut() -> O + 'static) -> UseCallback { + // Create a copyvalue with no contents + // This copyvalue is generic over F so that it can be sized properly + let mut inner = use_hook(|| CopyValue::new(None)); + + // Every time this hook is called replace the inner callback with the new callback + inner.set(Some(f)); + + // And then wrap that callback in a boxed callback so we're blind to the size of the actual callback + use_hook(|| UseCallback { + inner: CopyValue::new(Box::new(move || { + inner.with_mut(|f: &mut Option<_>| f.as_mut().unwrap()()) + })), + }) +} + +/// This callback is not generic over a return type so you can hold a bunch of callbacks at once +/// +/// If you need a callback that returns a value, you can simply wrap the closure you pass in that sets a value in its scope +#[derive(PartialEq)] +pub struct UseCallback { + inner: CopyValue O>>, +} + +impl Clone for UseCallback { + fn clone(&self) -> Self { + Self { inner: self.inner } + } +} +// impl Copy for UseCallback {} + +impl UseCallback { + /// Call the callback + pub fn call(&mut self) -> O { + self.inner.with_mut(|f| f()) + } +} diff --git a/packages/hooks/src/use_future.rs b/packages/hooks/src/use_future.rs index 5a7d955b3..a03863d81 100644 --- a/packages/hooks/src/use_future.rs +++ b/packages/hooks/src/use_future.rs @@ -1,62 +1,67 @@ #![allow(missing_docs)] use dioxus_core::{ - prelude::{spawn, use_drop, use_hook}, + prelude::{spawn, use_before_render, use_drop, use_hook}, ScopeState, Task, }; use dioxus_signals::*; +use dioxus_signals::{Readable, Writable}; use futures_util::{future, pin_mut, FutureExt}; use std::{any::Any, cell::Cell, future::Future, pin::Pin, rc::Rc, sync::Arc, task::Poll}; +use crate::use_callback; + /// A hook that allows you to spawn a future /// -/// Does not regenerate the future when dependencies change. If you're looking for a future that does, check out -/// `use_resource` instead. +/// Does not regenerate the future when dependencies change. pub fn use_future(mut future: impl FnMut() -> F) -> UseFuture where F: Future + 'static, { - let mut state = use_signal(|| UseFutureState::Pending); + let state = use_signal(|| UseFutureState::Pending); - let task = use_signal(|| { - // Create the user's task + // Create the task inside a copyvalue so we can reset it in-place later + let task = use_hook(|| { let fut = future(); - - // Spawn a wrapper task that polls the innner future and watch its dependencies - let task = spawn(async move { - // move the future here and pin it so we can poll it - let fut = fut; - pin_mut!(fut); - - let res = future::poll_fn(|cx| { - // Set the effect stack properly - - // Poll the inner future - let ready = fut.poll_unpin(cx); - - // add any dependencies to the effect stack that we need to watch when restarting the future - - ready - }) - .await; - - // Set the value - // value.set(Some(res)); - }); - - Some(task) + CopyValue::new(spawn(async move { + fut.await; + })) }); - use_drop(move || { - if let Some(task) = task.take() { - task.stop(); - } + /* + Early returns in dioxus have consequences for use_memo, use_resource, and use_future, etc + We *don't* want futures to be running if the component early returns. It's a rather weird behavior to have + use_memo running in the background even if the component isn't hitting those hooks anymore. + + React solves this by simply not having early returns interleave with hooks. + However, since dioxus allows early returns (since we use them for suspense), we need to solve this problem. + + + */ + // Track if this *current* render is the same + let gen = use_hook(|| CopyValue::new((0, 0))); + + // Early returns will pause this task, effectively + use_before_render(move || { + gen.write().0 += 1; + task.peek().set_active(false); }); + // However when we actually run this component, we want to resume the task + task.peek().set_active(true); + gen.write().1 += 1; + + // if the gens are different, we need to wake the task + if gen().0 != gen().1 { + task.peek().wake(); + } + + use_drop(move || task.peek().stop()); + UseFuture { task, state } } pub struct UseFuture { - task: Signal>, + task: CopyValue, state: Signal, } diff --git a/packages/signals/src/effect.rs b/packages/signals/src/effect.rs index 0d5b4c2cf..a4bc0ca52 100644 --- a/packages/signals/src/effect.rs +++ b/packages/signals/src/effect.rs @@ -2,7 +2,7 @@ use crate::write::*; use core::{self, fmt::Debug}; use dioxus_core::prelude::*; use futures_channel::mpsc::UnboundedSender; -use futures_util::StreamExt; +use futures_util::{future::Either, pin_mut, StreamExt}; use generational_box::GenerationalBoxId; use parking_lot::RwLock; use rustc_hash::FxHashMap; @@ -52,14 +52,38 @@ pub(crate) fn get_effect_ref() -> EffectStackRef { None => { let (sender, mut receiver) = futures_channel::mpsc::unbounded(); spawn_forever(async move { - while let Some(id) = receiver.next().await { + let mut queued_memos = Vec::new(); + + loop { + // Wait for a flush + // This gives a chance for effects to be updated in place and memos to compute their values + let flush_await = flush_sync(); + pin_mut!(flush_await); + + loop { + let res = + futures_util::future::select(&mut flush_await, receiver.next()).await; + + match res { + Either::Right((_queued, _)) => { + if let Some(task) = _queued { + queued_memos.push(task); + } + continue; + } + Either::Left(_flushed) => break, + } + } + EFFECT_STACK.with(|stack| { - let effect_mapping = stack.effect_mapping.read(); - if let Some(mut effect) = effect_mapping.get(&id).copied() { - tracing::trace!("Rerunning effect: {:?}", id); - effect.try_run(); - } else { - tracing::trace!("Effect not found: {:?}", id); + for id in queued_memos.drain(..) { + let effect_mapping = stack.effect_mapping.read(); + if let Some(mut effect) = effect_mapping.get(&id).copied() { + tracing::trace!("Rerunning effect: {:?}", id); + effect.try_run(); + } else { + tracing::trace!("Effect not found: {:?}", id); + } } }); }