瀏覽代碼

Add flush_sync and pre_render methods to core

Jonathan Kelley 1 年之前
父節點
當前提交
7b0dcb3206

+ 2 - 0
packages/core/Cargo.toml

@@ -14,12 +14,14 @@ rustc-hash = { workspace = true }
 longest-increasing-subsequence = "0.1.0"
 futures-util = { workspace = true, default-features = false, features = [
     "alloc",
+    "std",
 ] }
 slab = { workspace = true }
 futures-channel = { workspace = true }
 tracing = { workspace = true }
 serde = { version = "1", features = ["derive"], optional = true }
 
+
 [dev-dependencies]
 tokio = { workspace = true, features = ["full"] }
 dioxus = { workspace = true }

+ 2 - 10
packages/core/README.md

@@ -41,7 +41,7 @@ use dioxus::prelude::*;
 fn app() -> Element {
     rsx!{
         div { "hello world" }
-    })
+    }
 }
 
 fn main() {
@@ -49,13 +49,8 @@ fn main() {
     let mut dom = VirtualDom::new(app);
 
     // The initial render of the dom will generate a stream of edits for the real dom to apply
-    let mutations = dom.rebuild();
-
-    // Somehow, you can apply these edits to the real dom
-    apply_edits_to_real_dom(mutations);
+    let mutations = dom.rebuild_to_vec();
 }
-
-# fn apply_edits_to_real_dom(mutations: Mutations) {}
 ```
 
 
@@ -68,9 +63,6 @@ We can then wait for any asynchronous components or pending futures using the `w
 # async fn wait(mut dom: VirtualDom) {
 // Wait for the dom to be marked dirty internally
 dom.wait_for_work().await;
-
-// Or wait for a deadline and then collect edits
-let mutations = dom.render_with_deadline(tokio::time::sleep(Duration::from_millis(16)));
 # }
 ```
 

+ 1 - 1
packages/core/src/arena.rs

@@ -70,7 +70,7 @@ impl VirtualDom {
     pub(crate) fn drop_scope(&mut self, id: ScopeId) {
         let height = {
             let scope = self.scopes.remove(id.0);
-            let context = scope.context();
+            let context = scope.state();
             context.height
         };
 

+ 3 - 3
packages/core/src/diff/component.rs

@@ -82,7 +82,7 @@ impl VNode {
             tracing::trace!(
                 "Memoized props for component {:#?} ({})",
                 scope_id,
-                old_scope.context().name
+                old_scope.state().name
             );
             return;
         }
@@ -94,7 +94,7 @@ impl VNode {
         let new = dom.run_scope(scope_id);
         dom.diff_scope(to, scope_id, new);
 
-        let height = dom.runtime.get_context(scope_id).unwrap().height;
+        let height = dom.runtime.get_state(scope_id).unwrap().height;
         dom.dirty_scopes.remove(&DirtyScope {
             height,
             id: scope_id,
@@ -130,7 +130,7 @@ impl VNode {
         // Load up a ScopeId for this vcomponent. If it's already mounted, then we can just use that
         let scope = dom
             .new_scope(component.props.duplicate(), component.name)
-            .context()
+            .state()
             .id;
 
         // Store the scope id for the next render

+ 48 - 11
packages/core/src/global_context.rs

@@ -1,8 +1,6 @@
-use std::sync::Arc;
-
-use futures_util::Future;
-
 use crate::{runtime::Runtime, Element, ScopeId, Task};
+use futures_util::{future::poll_fn, Future};
+use std::sync::Arc;
 
 /// Get the current scope id
 pub fn current_scope_id() -> Option<ScopeId> {
@@ -30,7 +28,7 @@ pub fn consume_context<T: 'static + Clone>() -> T {
 /// Consume context from the current scope
 pub fn consume_context_from_scope<T: 'static + Clone>(scope_id: ScopeId) -> Option<T> {
     Runtime::with(|rt| {
-        rt.get_context(scope_id)
+        rt.get_state(scope_id)
             .and_then(|cx| cx.consume_context::<T>())
     })
     .flatten()
@@ -53,9 +51,7 @@ pub fn provide_root_context<T: 'static + Clone>(value: T) -> Option<T> {
 
 /// Suspends the current component
 pub fn suspend() -> Option<Element> {
-    Runtime::with_current_scope(|cx| {
-        cx.suspend();
-    });
+    Runtime::with_current_scope(|cx| cx.suspend());
     None
 }
 
@@ -87,7 +83,7 @@ pub fn remove_future(id: Task) {
 /// # Example
 ///
 /// ```
-/// use dioxus_core::ScopeState;
+/// use dioxus_core::use_hook;
 ///
 /// // prints a greeting on the initial render
 /// pub fn use_hello_world() {
@@ -98,6 +94,47 @@ pub fn use_hook<State: Clone + 'static>(initializer: impl FnOnce() -> State) ->
     Runtime::with_current_scope(|cx| cx.use_hook(initializer)).expect("to be in a dioxus runtime")
 }
 
+/// 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
+pub fn use_before_render(f: impl FnMut() + 'static) {
+    Runtime::with_current_scope(|cx| cx.push_before_render(f));
+}
+
+/// Wait for the virtualdom to finish its sync work before proceeding
+///
+/// This is useful if you've just triggered an update and want to wait for it to finish before proceeding with valid
+/// DOM nodes.
+pub async fn flush_sync() {
+    let mut polled = false;
+
+    let _task =
+        FlushKey(Runtime::with(|rt| rt.add_to_flush_table()).expect("to be in a dioxus runtime"));
+
+    // Poll without giving the waker to anyone
+    // The runtime will manually wake this task up when it's ready
+    poll_fn(|_| {
+        if !polled {
+            polled = true;
+            futures_util::task::Poll::Pending
+        } else {
+            futures_util::task::Poll::Ready(())
+        }
+    })
+    .await;
+
+    // If the the future got polled, then we don't need to prevent it from being dropped
+    // This would all be solved with generational indicies on tasks
+    std::mem::forget(_task);
+
+    struct FlushKey(Task);
+    impl Drop for FlushKey {
+        fn drop(&mut self) {
+            Runtime::with(|rt| rt.flush_table.borrow_mut().remove(&self.0));
+        }
+    }
+}
+
 /// Get the current render since the inception of this component
 ///
 /// This can be used as a helpful diagnostic when debugging hooks/renders, etc
@@ -138,7 +175,7 @@ pub fn schedule_update_any() -> Arc<dyn Fn(ScopeId) + Send + Sync> {
 /// (created with [`use_effect`](crate::use_effect)).
 ///
 /// Example:
-/// ```rust
+/// ```rust, ignore
 /// use dioxus::prelude::*;
 ///
 /// fn app() -> Element {
@@ -176,7 +213,7 @@ pub fn schedule_update_any() -> Arc<dyn Fn(ScopeId) + Send + Sync> {
 ///         }
 ///     });
 ///
-///     use_on_destroy({
+///     use_drop({
 ///         to_owned![original_scroll_position];
 ///         /// restore scroll to the top of the page
 ///         move || {

+ 9 - 8
packages/core/src/lib.rs

@@ -59,6 +59,7 @@ pub(crate) mod innerlude {
     ///     example()
     /// )
     /// ```
+    ///
     /// ## React-Style
     /// ```rust, ignore
     /// fn Example(cx: Props) -> Element {
@@ -86,14 +87,14 @@ pub use crate::innerlude::{
 /// This includes types like [`Scope`], [`Element`], and [`Component`].
 pub mod prelude {
     pub use crate::innerlude::{
-        consume_context, consume_context_from_scope, current_scope_id, fc_to_builder, generation,
-        has_context, needs_update, parent_scope, provide_context, provide_root_context,
+        consume_context, consume_context_from_scope, current_scope_id, fc_to_builder, flush_sync,
+        generation, has_context, needs_update, parent_scope, provide_context, provide_root_context,
         remove_future, schedule_update, schedule_update_any, spawn, spawn_forever, suspend,
-        try_consume_context, use_drop, use_error_boundary, use_hook, use_hook_with_cleanup,
-        AnyValue, Attribute, Component, ComponentFunction, Element, ErrorBoundary, Event,
-        EventHandler, Fragment, HasAttributes, IntoAttributeValue, IntoDynNode,
-        OptionStringFromMarker, Properties, Runtime, RuntimeGuard, ScopeId, ScopeState, SuperFrom,
-        SuperInto, Task, Template, TemplateAttribute, TemplateNode, Throw, VNode, VNodeInner,
-        VirtualDom,
+        try_consume_context, use_before_render, use_drop, use_error_boundary, use_hook,
+        use_hook_with_cleanup, AnyValue, Attribute, Component, ComponentFunction, Element,
+        ErrorBoundary, Event, EventHandler, Fragment, HasAttributes, IntoAttributeValue,
+        IntoDynNode, OptionStringFromMarker, Properties, Runtime, RuntimeGuard, ScopeId,
+        ScopeState, SuperFrom, SuperInto, Task, Template, TemplateAttribute, TemplateNode, Throw,
+        VNode, VNodeInner, VirtualDom,
     };
 }

+ 2 - 2
packages/core/src/nodes.rs

@@ -105,7 +105,7 @@ pub struct VNodeInner {
     /// The inner list *must* be in the format [static named attributes, remaining dynamically named attributes].
     ///
     /// For example:
-    /// ```rust
+    /// ```rust, ignore
     /// div {
     ///     class: "{class}",
     ///     ..attrs,
@@ -116,7 +116,7 @@ pub struct VNodeInner {
     /// ```
     ///
     /// Would be represented as:
-    /// ```rust
+    /// ```rust, ignore
     /// [
     ///     [class, every attribute in attrs sorted by name], // Slot 0 in the template
     ///     [color], // Slot 1 in the template

+ 36 - 41
packages/core/src/runtime.rs

@@ -1,12 +1,13 @@
+use rustc_hash::FxHashSet;
+
 use crate::{
     innerlude::{LocalTask, SchedulerMsg},
-    scope_context::ScopeContext,
+    scope_context::Scope,
     scopes::ScopeId,
     Task,
 };
 use std::{
     cell::{Cell, Ref, RefCell},
-    collections::VecDeque,
     rc::Rc,
 };
 
@@ -16,7 +17,7 @@ thread_local! {
 
 /// A global runtime that is shared across all scopes that provides the async runtime and context API
 pub struct Runtime {
-    pub(crate) scope_contexts: RefCell<Vec<Option<ScopeContext>>>,
+    pub(crate) scope_states: RefCell<Vec<Option<Scope>>>,
 
     // We use this to track the current scope
     pub(crate) scope_stack: RefCell<Vec<ScopeId>>,
@@ -29,10 +30,10 @@ pub struct Runtime {
     /// Tasks created with cx.spawn
     pub(crate) tasks: RefCell<slab::Slab<Rc<LocalTask>>>,
 
-    /// Queued tasks that are waiting to be polled
-    pub(crate) queued_tasks: Rc<RefCell<VecDeque<Task>>>,
-
     pub(crate) sender: futures_channel::mpsc::UnboundedSender<SchedulerMsg>,
+
+    // Tasks waiting to be manually resumed when we call wait_for_work
+    pub(crate) flush_table: RefCell<FxHashSet<Task>>,
 }
 
 impl Runtime {
@@ -40,11 +41,11 @@ impl Runtime {
         Rc::new(Self {
             sender,
             rendering: Cell::new(true),
-            scope_contexts: Default::default(),
+            scope_states: Default::default(),
             scope_stack: Default::default(),
             current_task: Default::default(),
             tasks: Default::default(),
-            queued_tasks: Rc::new(RefCell::new(VecDeque::new())),
+            flush_table: Default::default(),
         })
     }
 
@@ -54,35 +55,39 @@ impl Runtime {
     }
 
     /// Create a scope context. This slab is synchronized with the scope slab.
-    pub(crate) fn create_context_at(&self, id: ScopeId, context: ScopeContext) {
-        let mut contexts = self.scope_contexts.borrow_mut();
-        if contexts.len() <= id.0 {
-            contexts.resize_with(id.0 + 1, Default::default);
+    pub(crate) fn create_scope(&self, context: Scope) {
+        let id = context.id;
+        let mut scopes = self.scope_states.borrow_mut();
+        if scopes.len() <= id.0 {
+            scopes.resize_with(id.0 + 1, Default::default);
         }
-        contexts[id.0] = Some(context);
+        scopes[id.0] = Some(context);
     }
 
-    pub(crate) fn remove_context(self: &Rc<Self>, id: ScopeId) {
+    pub(crate) fn remove_scope(self: &Rc<Self>, id: ScopeId) {
         {
-            let borrow = self.scope_contexts.borrow();
+            let borrow = self.scope_states.borrow();
             if let Some(scope) = &borrow[id.0] {
                 let _runtime_guard = RuntimeGuard::new(self.clone());
                 // Manually drop tasks, hooks, and contexts inside of the runtime
                 self.on_scope(id, || {
-                    // Drop all spawned tasks
+                    // Drop all spawned tasks - order doesn't matter since tasks don't rely on eachother
+                    // In theory nested tasks might not like this
                     for id in scope.spawned_tasks.take() {
                         self.remove_task(id);
                     }
 
-                    // Drop all hooks
-                    scope.hooks.take();
+                    // Drop all hooks in reverse order in case a hook depends on another hook.
+                    for hook in scope.hooks.take().drain(..).rev() {
+                        drop(hook);
+                    }
 
                     // Drop all contexts
                     scope.shared_contexts.take();
                 });
             }
         }
-        self.scope_contexts.borrow_mut()[id.0].take();
+        self.scope_states.borrow_mut()[id.0].take();
     }
 
     /// Get the current scope id
@@ -104,11 +109,11 @@ impl Runtime {
         o
     }
 
-    /// Get the context for any scope given its ID
+    /// Get the state for any scope given its ID
     ///
     /// This is useful for inserting or removing contexts from a scope, or rendering out its root node
-    pub(crate) fn get_context(&self, id: ScopeId) -> Option<Ref<'_, ScopeContext>> {
-        Ref::filter_map(self.scope_contexts.borrow(), |contexts| {
+    pub(crate) fn get_state(&self, id: ScopeId) -> Option<Ref<'_, Scope>> {
+        Ref::filter_map(self.scope_states.borrow(), |contexts| {
             contexts.get(id.0).and_then(|f| f.as_ref())
         })
         .ok()
@@ -125,34 +130,22 @@ impl Runtime {
     }
 
     /// Runs a function with the current runtime
-    pub(crate) fn with<F, R>(f: F) -> Option<R>
-    where
-        F: FnOnce(&Runtime) -> R,
-    {
-        RUNTIMES.with(|stack| {
-            let stack = stack.borrow();
-            stack.last().map(|r| f(r))
-        })
+    pub(crate) fn with<R>(f: impl FnOnce(&Runtime) -> R) -> Option<R> {
+        RUNTIMES.with(|stack| stack.borrow().last().map(|r| f(r)))
     }
 
     /// Runs a function with the current scope
-    pub(crate) fn with_current_scope<F, R>(f: F) -> Option<R>
-    where
-        F: FnOnce(&ScopeContext) -> R,
-    {
+    pub(crate) fn with_current_scope<R>(f: impl FnOnce(&Scope) -> R) -> Option<R> {
         Self::with(|rt| {
             rt.current_scope_id()
-                .and_then(|scope| rt.get_context(scope).map(|sc| f(&sc)))
+                .and_then(|scope| rt.get_state(scope).map(|sc| f(&sc)))
         })
         .flatten()
     }
 
     /// Runs a function with the current scope
-    pub(crate) fn with_scope<F, R>(scope: ScopeId, f: F) -> Option<R>
-    where
-        F: FnOnce(&ScopeContext) -> R,
-    {
-        Self::with(|rt| rt.get_context(scope).map(|sc| f(&sc))).flatten()
+    pub(crate) fn with_scope<R>(scope: ScopeId, f: impl FnOnce(&Scope) -> R) -> Option<R> {
+        Self::with(|rt| rt.get_state(scope).map(|sc| f(&sc))).flatten()
     }
 }
 
@@ -182,7 +175,9 @@ impl Runtime {
 /// }
 ///
 /// fn Component(cx: ComponentProps) -> Element {
-///     cx.use_hook(|| RuntimeGuard::new(cx.runtime.clone()));
+///     use_hook(|| {
+///         let _guard = RuntimeGuard::new(cx.runtime.clone());
+///     });
 ///
 ///     rsx! { div {} }
 /// }

+ 18 - 9
packages/core/src/scope_arena.rs

@@ -2,7 +2,7 @@ use crate::{
     any_props::{AnyProps, BoxedAnyProps},
     innerlude::{DirtyScope, ScopeState},
     nodes::RenderReturn,
-    scope_context::ScopeContext,
+    scope_context::Scope,
     scopes::ScopeId,
     virtual_dom::VirtualDom,
 };
@@ -11,7 +11,7 @@ impl VirtualDom {
     pub(super) fn new_scope(&mut self, props: BoxedAnyProps, name: &'static str) -> &ScopeState {
         let parent_id = self.runtime.current_scope_id();
         let height = parent_id
-            .and_then(|parent_id| self.runtime.get_context(parent_id).map(|f| f.height + 1))
+            .and_then(|parent_id| self.runtime.get_state(parent_id).map(|f| f.height + 1))
             .unwrap_or(0);
         let entry = self.scopes.vacant_entry();
         let id = ScopeId(entry.key());
@@ -19,34 +19,43 @@ impl VirtualDom {
         let scope = entry.insert(ScopeState {
             runtime: self.runtime.clone(),
             context_id: id,
-
             props,
             last_rendered_node: Default::default(),
         });
 
-        let context = ScopeContext::new(name, id, parent_id, height);
-        self.runtime.create_context_at(id, context);
+        self.runtime
+            .create_scope(Scope::new(name, id, parent_id, height));
 
         scope
     }
 
     pub(crate) fn run_scope(&mut self, scope_id: ScopeId) -> RenderReturn {
-        self.runtime.scope_stack.borrow_mut().push(scope_id);
+        debug_assert!(
+            crate::Runtime::current().is_some(),
+            "Must be in a dioxus runtime"
+        );
 
+        self.runtime.scope_stack.borrow_mut().push(scope_id);
         let scope = &self.scopes[scope_id.0];
         let new_nodes = {
-            let context = scope.context();
+            let context = scope.state();
+
             context.suspended.set(false);
             context.hook_index.set(0);
 
+            // Run all pre-render hooks
+            for pre_run in context.before_render.borrow_mut().iter_mut() {
+                pre_run();
+            }
+
             // safety: due to how we traverse the tree, we know that the scope is not currently aliased
             let props: &dyn AnyProps = &*scope.props;
 
-            let span = tracing::trace_span!("render", scope = %scope.context().name);
+            let span = tracing::trace_span!("render", scope = %scope.state().name);
             span.in_scope(|| props.render())
         };
 
-        let context = scope.context();
+        let context = scope.state();
 
         // And move the render generation forward by one
         context.render_count.set(context.render_count.get() + 1);

+ 13 - 8
packages/core/src/scope_context.rs

@@ -10,7 +10,7 @@ use std::{
 /// A component's state separate from its props.
 ///
 /// This struct exists to provide a common interface for all scopes without relying on generics.
-pub(crate) struct ScopeContext {
+pub(crate) struct Scope {
     pub(crate) name: &'static str,
     pub(crate) id: ScopeId,
     pub(crate) parent_id: Option<ScopeId>,
@@ -21,13 +21,12 @@ pub(crate) struct ScopeContext {
     // Note: the order of the hook and context fields is important. The hooks field must be dropped before the contexts field in case a hook drop implementation tries to access a context.
     pub(crate) hooks: RefCell<Vec<Box<dyn Any>>>,
     pub(crate) hook_index: Cell<usize>,
-
     pub(crate) shared_contexts: RefCell<Vec<Box<dyn Any>>>,
-
     pub(crate) spawned_tasks: RefCell<FxHashSet<Task>>,
+    pub(crate) before_render: RefCell<Vec<Box<dyn FnMut()>>>,
 }
 
-impl ScopeContext {
+impl Scope {
     pub(crate) fn new(
         name: &'static str,
         id: ScopeId,
@@ -45,6 +44,7 @@ impl ScopeContext {
             spawned_tasks: RefCell::new(FxHashSet::default()),
             hooks: RefCell::new(vec![]),
             hook_index: Cell::new(0),
+            before_render: RefCell::new(vec![]),
         }
     }
 
@@ -109,7 +109,7 @@ impl ScopeContext {
         let mut search_parent = self.parent_id;
         let cur_runtime = Runtime::with(|runtime| {
             while let Some(parent_id) = search_parent {
-                let parent = runtime.get_context(parent_id).unwrap();
+                let parent = runtime.get_state(parent_id).unwrap();
                 tracing::trace!(
                     "looking for context {} ({:?}) in {}",
                     std::any::type_name::<T>(),
@@ -212,7 +212,7 @@ impl ScopeContext {
     pub fn provide_root_context<T: 'static + Clone>(&self, context: T) -> T {
         Runtime::with(|runtime| {
             runtime
-                .get_context(ScopeId::ROOT)
+                .get_state(ScopeId::ROOT)
                 .unwrap()
                 .provide_context(context)
         })
@@ -256,9 +256,10 @@ impl ScopeContext {
     /// # Example
     ///
     /// ```
+    /// # use dioxus::prelude::*;
     /// // prints a greeting on the initial render
     /// pub fn use_hello_world() {
-    ///     cx.use_hook(|| println!("Hello, world!"));
+    ///     use_hook(|| println!("Hello, world!"));
     /// }
     /// ```
     pub fn use_hook<State: Clone + 'static>(&self, initializer: impl FnOnce() -> State) -> State {
@@ -287,6 +288,10 @@ impl ScopeContext {
             )
     }
 
+    pub fn push_before_render(&self, f: impl FnMut() + 'static) {
+        self.before_render.borrow_mut().push(Box::new(f));
+    }
+
     /// Get the current render since the inception of this component
     ///
     /// This can be used as a helpful diagnostic when debugging hooks/renders, etc
@@ -320,7 +325,7 @@ impl ScopeId {
     /// Consume context from the current scope
     pub fn consume_context_from_scope<T: 'static + Clone>(self, scope_id: ScopeId) -> Option<T> {
         Runtime::with(|rt| {
-            rt.get_context(scope_id)
+            rt.get_state(scope_id)
                 .and_then(|cx| cx.consume_context::<T>())
         })
         .flatten()

+ 4 - 4
packages/core/src/scopes.rs

@@ -1,5 +1,5 @@
 use crate::{
-    any_props::BoxedAnyProps, nodes::RenderReturn, runtime::Runtime, scope_context::ScopeContext,
+    any_props::BoxedAnyProps, nodes::RenderReturn, runtime::Runtime, scope_context::Scope,
 };
 use std::{cell::Ref, fmt::Debug, rc::Rc};
 
@@ -39,7 +39,7 @@ pub struct ScopeState {
 
 impl Drop for ScopeState {
     fn drop(&mut self) {
-        self.runtime.remove_context(self.context_id);
+        self.runtime.remove_scope(self.context_id);
     }
 }
 
@@ -63,7 +63,7 @@ impl ScopeState {
         self.last_rendered_node.as_ref()
     }
 
-    pub(crate) fn context(&self) -> Ref<'_, ScopeContext> {
-        self.runtime.get_context(self.context_id).unwrap()
+    pub(crate) fn state(&self) -> Ref<'_, Scope> {
+        self.runtime.get_state(self.context_id).unwrap()
     }
 }

+ 53 - 34
packages/core/src/tasks.rs

@@ -1,10 +1,10 @@
 use crate::innerlude::{remove_future, spawn, Runtime};
 use crate::ScopeId;
 use futures_util::task::ArcWake;
-use std::future::Future;
 use std::pin::Pin;
 use std::sync::Arc;
 use std::task::Waker;
+use std::{cell::Cell, future::Future};
 use std::{cell::RefCell, rc::Rc};
 
 /// A task's unique identifier.
@@ -34,6 +34,25 @@ impl Task {
     pub fn stop(self) {
         remove_future(self);
     }
+
+    /// Pause the task.
+    pub fn pause(&self) {
+        Runtime::with(|rt| rt.tasks.borrow()[self.0].active.set(false));
+    }
+
+    /// Check if the task is paused.
+    pub fn paused(&self) -> bool {
+        Runtime::with(|rt| !rt.tasks.borrow()[self.0].active.get()).unwrap_or_default()
+    }
+
+    /// 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));
+        });
+    }
 }
 
 impl Runtime {
@@ -55,9 +74,10 @@ impl Runtime {
             let task_id = Task(entry.key());
 
             let task = Rc::new(LocalTask {
+                scope,
+                active: Cell::new(true),
                 parent: self.current_task(),
                 task: RefCell::new(Box::pin(task)),
-                scope,
                 waker: futures_util::task::waker(Arc::new(LocalTaskHandle {
                     id: task_id,
                     tx: self.sender.clone(),
@@ -73,18 +93,33 @@ impl Runtime {
         debug_assert!(self.tasks.try_borrow_mut().is_ok());
         debug_assert!(task.task.try_borrow_mut().is_ok());
 
-        let mut cx = std::task::Context::from_waker(&task.waker);
-
-        if !task.task.borrow_mut().as_mut().poll(&mut cx).is_ready() {
-            self.sender
-                .unbounded_send(SchedulerMsg::TaskNotified(task_id))
-                .expect("Scheduler should exist");
-        }
+        self.sender
+            .unbounded_send(SchedulerMsg::TaskNotified(task_id))
+            .expect("Scheduler should exist");
 
         task_id
     }
 
+    /// Get the currently running task
+    pub fn current_task(&self) -> Option<Task> {
+        self.current_task.get()
+    }
+
+    /// Get the parent task of the given task, if it exists
+    pub fn parent_task(&self, task: Task) -> Option<Task> {
+        self.tasks.borrow().get(task.0)?.parent
+    }
+
+    /// Add this task to the queue of tasks that will manually get poked when the scheduler is flushed
+    pub(crate) fn add_to_flush_table(&self) -> Task {
+        let value = self.current_task().unwrap();
+        self.flush_table.borrow_mut().insert(value);
+        value
+    }
+
     pub(crate) fn handle_task_wakeup(&self, id: Task) {
+        debug_assert!(Runtime::current().is_some(), "Must be in a dioxus runtime");
+
         let task = self.tasks.borrow().get(id.0).cloned();
 
         // The task was removed from the scheduler, so we can just ignore it
@@ -92,6 +127,11 @@ impl Runtime {
             return;
         };
 
+        // If a task woke up but is paused, we can just ignore it
+        if !task.active.get() {
+            return;
+        }
+
         let mut cx = std::task::Context::from_waker(&task.waker);
 
         // update the scope stack
@@ -99,10 +139,9 @@ impl Runtime {
         self.rendering.set(false);
         self.current_task.set(Some(id));
 
-        // If the task completes...
         if task.task.borrow_mut().as_mut().poll(&mut cx).is_ready() {
             // Remove it from the scope so we dont try to double drop it when the scope dropes
-            self.get_context(task.scope)
+            self.get_state(task.scope)
                 .unwrap()
                 .spawned_tasks
                 .borrow_mut()
@@ -118,31 +157,11 @@ impl Runtime {
         self.current_task.set(None);
     }
 
-    /// Take a queued task from the scheduler
-    pub(crate) fn take_queued_task(&self) -> Option<Task> {
-        self.queued_tasks.borrow_mut().pop_front()
-    }
-
     /// Drop the future with the given TaskId
     ///
     /// This does not abort the task, so you'll want to wrap it in an abort handle if that's important to you
     pub(crate) fn remove_task(&self, id: Task) -> Option<Rc<LocalTask>> {
-        let task = self.tasks.borrow_mut().try_remove(id.0);
-
-        // Remove the task from the queued tasks so we don't poll a different task with the same id
-        self.queued_tasks.borrow_mut().retain(|t| *t != id);
-
-        task
-    }
-
-    /// Get the currently running task
-    pub fn current_task(&self) -> Option<Task> {
-        self.current_task.get()
-    }
-
-    /// Get the parent task of the given task, if it exists
-    pub fn parent_task(&self, task: Task) -> Option<Task> {
-        self.tasks.borrow().get(task.0)?.parent
+        self.tasks.borrow_mut().try_remove(id.0)
     }
 }
 
@@ -152,6 +171,7 @@ pub(crate) struct LocalTask {
     parent: Option<Task>,
     task: RefCell<Pin<Box<dyn Future<Output = ()> + 'static>>>,
     waker: Waker,
+    active: Cell<bool>,
 }
 
 /// The type of message that can be sent to the scheduler.
@@ -173,8 +193,7 @@ struct LocalTaskHandle {
 
 impl ArcWake for LocalTaskHandle {
     fn wake_by_ref(arc_self: &Arc<Self>) {
-        // This can fail if the scheduler has been dropped while the application is shutting down
-        let _ = arc_self
+        _ = arc_self
             .tx
             .unbounded_send(SchedulerMsg::TaskNotified(arc_self.id));
     }

+ 75 - 132
packages/core/src/virtual_dom.rs

@@ -13,12 +13,12 @@ use crate::{
     nodes::{Template, TemplateId},
     runtime::{Runtime, RuntimeGuard},
     scopes::ScopeId,
-    AttributeValue, ComponentFunction, Element, Event, Mutations, Task,
+    AttributeValue, ComponentFunction, Element, Event, Mutations,
 };
-use futures_util::{pin_mut, StreamExt};
+use futures_util::StreamExt;
 use rustc_hash::{FxHashMap, FxHashSet};
 use slab::Slab;
-use std::{any::Any, collections::BTreeSet, future::Future, rc::Rc};
+use std::{any::Any, collections::BTreeSet, rc::Rc};
 
 /// A virtual node system that progresses user events and diffs UI trees.
 ///
@@ -29,7 +29,7 @@ use std::{any::Any, collections::BTreeSet, future::Future, rc::Rc};
 /// ```rust
 /// # use dioxus::prelude::*;
 ///
-/// #[derive(Props, PartialEq)]
+/// #[derive(Props, PartialEq, Clone)]
 /// struct AppProps {
 ///     title: String
 /// }
@@ -37,7 +37,7 @@ use std::{any::Any, collections::BTreeSet, future::Future, rc::Rc};
 /// fn app(cx: AppProps) -> Element {
 ///     rsx!(
 ///         div {"hello, {cx.title}"}
-///     ))
+///     )
 /// }
 /// ```
 ///
@@ -47,7 +47,7 @@ use std::{any::Any, collections::BTreeSet, future::Future, rc::Rc};
 /// # #![allow(unused)]
 /// # use dioxus::prelude::*;
 ///
-/// # #[derive(Props, PartialEq)]
+/// # #[derive(Props, PartialEq, Clone)]
 /// # struct AppProps {
 /// #     title: String
 /// # }
@@ -60,26 +60,26 @@ use std::{any::Any, collections::BTreeSet, future::Future, rc::Rc};
 ///         NavBar { routes: ROUTES }
 ///         Title { "{cx.title}" }
 ///         Footer {}
-///     ))
+///     )
 /// }
 ///
 /// #[component]
 /// fn NavBar( routes: &'static str) -> Element {
 ///     rsx! {
 ///         div { "Routes: {routes}" }
-///     })
+///     }
 /// }
 ///
 /// #[component]
 /// fn Footer() -> Element {
-///     rsx! { div { "Footer" } })
+///     rsx! { div { "Footer" } }
 /// }
 ///
 /// #[component]
-/// fn Title<'a>( children: Element) -> Element {
+/// fn Title( children: Element) -> Element {
 ///     rsx! {
 ///         div { id: "title", {children} }
-///     })
+///     }
 /// }
 /// ```
 ///
@@ -88,10 +88,10 @@ use std::{any::Any, collections::BTreeSet, future::Future, rc::Rc};
 ///
 /// ```rust
 /// # use dioxus::prelude::*;
-/// # fn app() -> Element { rsx! { div {} }) }
+/// # fn app() -> Element { rsx! { div {} } }
 ///
 /// let mut vdom = VirtualDom::new(app);
-/// let edits = vdom.rebuild();
+/// let edits = vdom.rebuild_to_vec();
 /// ```
 ///
 /// To call listeners inside the VirtualDom, call [`VirtualDom::handle_event`] with the appropriate event data.
@@ -130,7 +130,7 @@ use std::{any::Any, collections::BTreeSet, future::Future, rc::Rc};
 /// fn app() -> Element {
 ///     rsx! {
 ///         div { "Hello World" }
-///     })
+///     }
 /// }
 ///
 /// let dom = VirtualDom::new(app);
@@ -247,7 +247,7 @@ impl VirtualDom {
     /// }
     ///
     /// fn Example(cx: SomeProps) -> Element  {
-    ///     rsx!{ div{ "hello {cx.name}" } })
+    ///     rsx!{ div { "hello {cx.name}" } }
     /// }
     ///
     /// let dom = VirtualDom::new(Example);
@@ -291,7 +291,7 @@ impl VirtualDom {
     /// }
     ///
     /// fn Example(cx: SomeProps) -> Element  {
-    ///     rsx!{ div{ "hello {cx.name}" } })
+    ///     rsx!{ div{ "hello {cx.name}" } }
     /// }
     ///
     /// let dom = VirtualDom::new(Example);
@@ -321,7 +321,7 @@ impl VirtualDom {
         let root = dom.new_scope(Box::new(root), "app");
 
         // Unlike react, we provide a default error boundary that just renders the error as a string
-        root.context()
+        root.state()
             .provide_context(Rc::new(ErrorBoundary::new_in_scope(ScopeId::ROOT)));
 
         // the root element is always given element ID 0 since it's the container for the entire tree
@@ -354,7 +354,7 @@ impl VirtualDom {
     ///
     /// This is useful for what is essentially dependency injection when building the app
     pub fn with_root_context<T: Clone + 'static>(self, context: T) -> Self {
-        self.base_scope().context().provide_context(context);
+        self.base_scope().state().provide_context(context);
         self
     }
 
@@ -362,14 +362,14 @@ impl VirtualDom {
     ///
     /// This method is useful for when you want to provide a context in your app without knowing its type
     pub fn insert_any_root_context(&mut self, context: Box<dyn Any>) {
-        self.base_scope().context().provide_any_context(context);
+        self.base_scope().state().provide_any_context(context);
     }
 
     /// Manually mark a scope as requiring a re-render
     ///
     /// Whenever the Runtime "works", it will re-render this scope
     pub fn mark_dirty(&mut self, id: ScopeId) {
-        if let Some(context) = self.runtime.get_context(id) {
+        if let Some(context) = self.runtime.get_state(id) {
             let height = context.height();
             tracing::trace!("Marking scope {:?} ({}) as dirty", id, context.name);
             self.dirty_scopes.insert(DirtyScope { height, id });
@@ -417,52 +417,50 @@ impl VirtualDom {
     ///
     /// ```rust, ignore
     /// let dom = VirtualDom::new(app);
-    /// let sender = dom.get_scheduler_channel();
     /// ```
     pub async fn wait_for_work(&mut self) {
-        let mut some_msg = None;
+        // Ping tasks waiting on the flush table - they're waiting for sync stuff to be done before progressing
+        self.clear_flush_table();
 
         loop {
-            // If a bunch of messages are ready in a sequence, try to pop them off synchronously
-            if let Some(msg) = some_msg.take() {
-                match msg {
-                    SchedulerMsg::Immediate(id) => self.mark_dirty(id),
-                    SchedulerMsg::TaskNotified(task) => self.queue_task_wakeup(task),
-                }
+            // Process all events - Scopes are marked dirty, etc
+            self.process_events();
+
+            // Now that we have collected all queued work, we should check if we have any dirty scopes. If there are not, then we can poll any queued futures
+            if !self.dirty_scopes.is_empty() || !self.suspended_scopes.is_empty() {
+                return;
             }
 
-            // If they're not ready, then we should wait for them to be ready
-            match self.rx.try_next() {
-                Ok(Some(val)) => some_msg = Some(val),
-                Ok(None) => return,
-                Err(_) => {
-                    // Now that we have collected all queued work, we should check if we have any dirty scopes. If there are not, then we can poll any queued futures
-                    let has_dirty_scopes = !self.dirty_scopes.is_empty();
-
-                    if !has_dirty_scopes {
-                        // If we have no dirty scopes, then we should poll any tasks that have been notified
-                        while let Some(task) = self.runtime.take_queued_task() {
-                            self.handle_task_wakeup(task);
-                        }
-                    }
+            // Make sure we set the runtime since we're running user code
+            let _runtime = RuntimeGuard::new(self.runtime.clone());
 
-                    // If we have any dirty scopes, or finished fiber trees then we should exit
-                    if has_dirty_scopes || !self.suspended_scopes.is_empty() {
-                        return;
-                    }
+            match self.rx.next().await.expect("channel should never close") {
+                SchedulerMsg::Immediate(id) => self.mark_dirty(id),
+                SchedulerMsg::TaskNotified(id) => self.runtime.handle_task_wakeup(id),
+            };
+        }
+    }
 
-                    some_msg = self.rx.next().await
-                }
-            }
+    fn clear_flush_table(&mut self) {
+        // Make sure we set the runtime since we're running user code
+        let _runtime = RuntimeGuard::new(self.runtime.clone());
+
+        // Manually flush tasks that called `flush().await`
+        // Tasks that might've been waiting for `flush` finally have a chance to run to their next await point
+        for task in self.runtime.flush_table.take() {
+            self.runtime.handle_task_wakeup(task);
         }
     }
 
     /// Process all events in the queue until there are no more left
     pub fn process_events(&mut self) {
+        let _runtime = RuntimeGuard::new(self.runtime.clone());
+
+        // Prevent a task from deadlocking the runtime by repeatedly queueing itself
         while let Ok(Some(msg)) = self.rx.try_next() {
             match msg {
                 SchedulerMsg::Immediate(id) => self.mark_dirty(id),
-                SchedulerMsg::TaskNotified(task) => self.queue_task_wakeup(task),
+                SchedulerMsg::TaskNotified(task) => self.runtime.handle_task_wakeup(task),
             }
         }
     }
@@ -481,7 +479,7 @@ impl VirtualDom {
                 if sync.template.get().name.rsplit_once(':').unwrap().0
                     == template.name.rsplit_once(':').unwrap().0
                 {
-                    let context = scope.context();
+                    let context = scope.state();
                     let height = context.height;
                     self.dirty_scopes.insert(DirtyScope {
                         height,
@@ -542,18 +540,25 @@ impl VirtualDom {
     /// suspended subtrees.
     pub fn render_immediate(&mut self, to: &mut impl WriteMutations) {
         self.flush_templates(to);
-        // Build a waker that won't wake up since our deadline is already expired when it's polled
-        let waker = futures_util::task::noop_waker();
-        let mut cx = std::task::Context::from_waker(&waker);
-
-        // Now run render with deadline but dont even try to poll any async tasks
-        let fut = self.render_with_deadline(std::future::ready(()), to);
-        pin_mut!(fut);
-
-        // The root component is not allowed to be async
-        match fut.poll(&mut cx) {
-            std::task::Poll::Ready(mutations) => mutations,
-            std::task::Poll::Pending => panic!("render_immediate should never return pending"),
+
+        // Process any events that might be pending in the queue
+        self.process_events();
+
+        // Next, diff any dirty scopes
+        // We choose not to poll the deadline since we complete pretty quickly anyways
+        while let Some(dirty) = self.dirty_scopes.pop_first() {
+            // If the scope doesn't exist for whatever reason, then we should skip it
+            if !self.scopes.contains(dirty.id.0) {
+                continue;
+            }
+
+            {
+                let _runtime = RuntimeGuard::new(self.runtime.clone());
+                // Run the scope and get the mutations
+                let new_nodes = self.run_scope(dirty.id);
+
+                self.diff_scope(to, dirty.id, new_nodes);
+            }
         }
     }
 
@@ -567,72 +572,24 @@ impl VirtualDom {
     /// Render the virtual dom, waiting for all suspense to be finished
     ///
     /// The mutations will be thrown out, so it's best to use this method for things like SSR that have async content
+    ///
+    /// Tasks waiting to be flushed are *cleared* here *without running them*
+    /// This behavior is subject to change, but configured this way so use_future/use_memo/use_future won't react on the server
     pub async fn wait_for_suspense(&mut self) {
         loop {
             if self.suspended_scopes.is_empty() {
                 return;
             }
 
+            // not sure if we should be doing this?
+            self.runtime.flush_table.borrow_mut().clear();
+
             self.wait_for_work().await;
 
             self.render_immediate(&mut NoOpMutations);
         }
     }
 
-    /// Render what you can given the timeline and then move on
-    ///
-    /// It's generally a good idea to put some sort of limit on the suspense process in case a future is having issues.
-    ///
-    /// If no suspense trees are present
-    pub async fn render_with_deadline(
-        &mut self,
-        deadline: impl Future<Output = ()>,
-        to: &mut impl WriteMutations,
-    ) {
-        self.flush_templates(to);
-        pin_mut!(deadline);
-
-        self.process_events();
-
-        loop {
-            // Next, diff any dirty scopes
-            // We choose not to poll the deadline since we complete pretty quickly anyways
-            while let Some(dirty) = self.dirty_scopes.pop_first() {
-                // If the scope doesn't exist for whatever reason, then we should skip it
-                if !self.scopes.contains(dirty.id.0) {
-                    continue;
-                }
-
-                {
-                    let _runtime = RuntimeGuard::new(self.runtime.clone());
-                    // Run the scope and get the mutations
-                    let new_nodes = self.run_scope(dirty.id);
-
-                    self.diff_scope(to, dirty.id, new_nodes);
-                }
-            }
-
-            // Wait until the deadline is ready or we have work if there's no work ready
-            let work = self.wait_for_work();
-            pin_mut!(work);
-
-            use futures_util::future::{select, Either};
-            if let Either::Left((_, _)) = select(&mut deadline, &mut work).await {
-                return;
-            }
-        }
-    }
-
-    /// [`Self::render_with_deadline`] to a vector of mutations for testing purposes
-    pub async fn render_with_deadline_to_vec(
-        &mut self,
-        deadline: impl Future<Output = ()>,
-    ) -> Mutations {
-        let mut mutations = Mutations::default();
-        self.render_with_deadline(deadline, &mut mutations).await;
-        mutations
-    }
-
     /// Get the current runtime
     pub fn runtime(&self) -> Rc<Runtime> {
         self.runtime.clone()
@@ -645,20 +602,6 @@ impl VirtualDom {
         }
     }
 
-    /// Queue a task to be polled after all dirty scopes have been rendered
-    fn queue_task_wakeup(&mut self, id: Task) {
-        self.runtime.queued_tasks.borrow_mut().push_back(id);
-    }
-
-    /// Handle notifications by tasks inside the scheduler
-    ///
-    /// This is precise, meaning we won't poll every task, just tasks that have woken up as notified to use by the
-    /// queue
-    fn handle_task_wakeup(&mut self, id: Task) {
-        let _runtime = RuntimeGuard::new(self.runtime.clone());
-        self.runtime.handle_task_wakeup(id);
-    }
-
     /*
     ------------------------
     The algorithm works by walking through the list of dynamic attributes, checking their paths, and breaking when
@@ -764,7 +707,7 @@ impl Drop for VirtualDom {
     fn drop(&mut self) {
         // Drop all scopes in order of height
         let mut scopes = self.scopes.drain().collect::<Vec<_>>();
-        scopes.sort_by_key(|scope| scope.context().height);
+        scopes.sort_by_key(|scope| scope.state().height);
         for scope in scopes.into_iter().rev() {
             drop(scope);
         }

+ 101 - 11
packages/core/tests/task.rs

@@ -1,11 +1,22 @@
 //! Verify that tasks get polled by the virtualdom properly, and that we escape wait_for_work safely
 
-#[cfg(not(miri))]
+use std::{sync::atomic::AtomicUsize, time::Duration};
+
+use dioxus::prelude::*;
+
+async fn run_vdom(app: fn() -> Element) {
+    let mut dom = VirtualDom::new(app);
+
+    dom.rebuild(&mut dioxus_core::NoOpMutations);
+
+    tokio::select! {
+        _ = dom.wait_for_work() => {}
+        _ = tokio::time::sleep(Duration::from_millis(500)) => {}
+    };
+}
+
 #[tokio::test]
 async fn it_works() {
-    use dioxus::prelude::*;
-    use std::{sync::atomic::AtomicUsize, time::Duration};
-
     static POLL_COUNT: AtomicUsize = AtomicUsize::new(0);
 
     fn app() -> Element {
@@ -28,19 +39,98 @@ async fn it_works() {
         rsx!({ () })
     }
 
+    run_vdom(app).await;
+
+    // By the time the tasks are finished, we should've accumulated ticks from two tasks
+    // Be warned that by setting the delay to too short, tokio might not schedule in the tasks
+    assert_eq!(
+        POLL_COUNT.fetch_add(0, std::sync::atomic::Ordering::Relaxed),
+        135
+    );
+}
+
+/// Prove that yield_now doesn't cause a deadlock
+#[tokio::test]
+async fn yield_now_works() {
+    thread_local! {
+        static SEQUENCE: std::cell::RefCell<Vec<usize>> = std::cell::RefCell::new(Vec::new());
+    }
+
+    fn app() -> Element {
+        // these two tasks should yield to eachother
+        use_hook(|| {
+            spawn(async move {
+                for x in 0..10 {
+                    tokio::task::yield_now().await;
+                    SEQUENCE.with(|s| s.borrow_mut().push(1));
+                }
+            })
+        });
+
+        use_hook(|| {
+            spawn(async move {
+                for x in 0..10 {
+                    tokio::task::yield_now().await;
+                    SEQUENCE.with(|s| s.borrow_mut().push(2));
+                }
+            })
+        });
+
+        rsx!({ () })
+    }
+
+    run_vdom(app).await;
+
+    SEQUENCE.with(|s| assert_eq!(s.borrow().len(), 20));
+}
+
+/// Ensure that calling wait_for_flush waits for dioxus to finish its syncrhonous work
+#[tokio::test]
+async fn flushing() {
+    thread_local! {
+        static SEQUENCE: std::cell::RefCell<Vec<usize>> = std::cell::RefCell::new(Vec::new());
+    }
+
+    fn app() -> Element {
+        use_hook(|| {
+            spawn(async move {
+                for x in 0..10 {
+                    flush_sync().await;
+                    SEQUENCE.with(|s| s.borrow_mut().push(1));
+                }
+            })
+        });
+
+        use_hook(|| {
+            spawn(async move {
+                for x in 0..10 {
+                    flush_sync().await;
+                    SEQUENCE.with(|s| s.borrow_mut().push(2));
+                }
+            })
+        });
+
+        rsx!({ () })
+    }
+
     let mut dom = VirtualDom::new(app);
 
     dom.rebuild(&mut dioxus_core::NoOpMutations);
 
+    let fut = async {
+        // Trigger the flush by waiting for work
+        for _ in 0..40 {
+            tokio::select! {
+                _ = dom.wait_for_work() => {}
+                _ = tokio::time::sleep(Duration::from_millis(1)) => {}
+            };
+        }
+    };
+
     tokio::select! {
-        _ = dom.wait_for_work() => {}
+        _ = fut => {}
         _ = tokio::time::sleep(Duration::from_millis(500)) => {}
     };
 
-    // By the time the tasks are finished, we should've accumulated ticks from two tasks
-    // Be warned that by setting the delay to too short, tokio might not schedule in the tasks
-    assert_eq!(
-        POLL_COUNT.fetch_add(0, std::sync::atomic::Ordering::Relaxed),
-        135
-    );
+    SEQUENCE.with(|s| assert_eq!(s.borrow().len(), 20));
 }

+ 3 - 3
packages/dioxus-tui/benches/update.rs

@@ -65,7 +65,7 @@ struct BoxProps {
     alpha: f32,
 }
 #[allow(non_snake_case)]
-fn Box(cx: Scope<BoxProps>) -> Element {
+fn Box(cx: ScopeState<BoxProps>) -> Element {
     let count = use_signal(|| 0);
 
     let x = cx.props.x * 2;
@@ -94,7 +94,7 @@ struct GridProps {
     update_count: usize,
 }
 #[allow(non_snake_case)]
-fn Grid(cx: Scope<GridProps>) -> Element {
+fn Grid(cx: ScopeState<GridProps>) -> Element {
     let size = cx.props.size;
     let count = use_signal(|| 0);
     let counts = use_signal(|| vec![0; size * size]);
@@ -151,7 +151,7 @@ fn Grid(cx: Scope<GridProps>) -> Element {
     }
 }
 
-fn app(cx: Scope<GridProps>) -> Element {
+fn app(cx: ScopeState<GridProps>) -> Element {
     rsx! {
         div{
             width: "100%",

+ 9 - 6
packages/liveview/src/pool.rs

@@ -6,7 +6,7 @@ use crate::{
     LiveViewError,
 };
 use dioxus_core::prelude::*;
-use dioxus_html::{EventData, HtmlEvent, PlatformEventData};
+use dioxus_html::{select, EventData, HtmlEvent, PlatformEventData};
 use dioxus_interpreter_js::MutationState;
 use futures_util::{pin_mut, SinkExt, StreamExt};
 use serde::Serialize;
@@ -227,11 +227,14 @@ pub async fn run(mut vdom: VirtualDom, ws: impl LiveViewSocket) -> Result<(), Li
             }
         }
 
-        vdom.render_with_deadline(
-            tokio::time::sleep(Duration::from_millis(10)),
-            &mut mutations,
-        )
-        .await;
+        // wait for suspense to resolve in a 10ms window
+        tokio::select! {
+            _ = tokio::time::sleep(Duration::from_millis(10)) => {}
+            _ = vdom.wait_for_suspense() => {}
+        }
+
+        // render the vdom
+        vdom.render_immediate(&mut mutations);
 
         if let Some(edits) = take_edits(&mut mutations) {
             ws.send(edits).await?;

+ 1 - 1
packages/router/src/components/router.rs

@@ -151,7 +151,7 @@ where
 
 #[cfg(feature = "serde")]
 /// A component that renders the current route.
-pub fn Router<R: Routable + Clone>(cx: Scope<RouterProps<R>>) -> Element
+pub fn Router<R: Routable + Clone>(cx: ScopeState<RouterProps<R>>) -> Element
 where
     <R as FromStr>::Err: std::fmt::Display,
     R: serde::Serialize + serde::de::DeserializeOwned,