From fc455b5530be53ebaf3491cabcd02af95bd94a4d Mon Sep 17 00:00:00 2001 From: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com> Date: Tue, 16 Jun 2026 22:16:24 +0000 Subject: [PATCH 01/16] refactor(core): extract RenderDriver trait from PR #5554 Replace the type-erased AnyProps (BoxedAnyProps / Box) approach with a trait-based RenderDriver abstraction for component lifecycle dispatch. Key changes: - New render_driver.rs: RenderDriver trait + BodyDriver (plain components) + DynWriter (sized wrapper bridging dyn WriteMutations into the generic diff pipeline) - VComponent now holds Rc instead of BoxedAnyProps + render_fn - ScopeState no longer stores props directly; the driver owns them - scope_arena: new_scope() takes an Rc; run_scope_with() replaces the old props-based run_scope() - diff/component.rs: create/diff/remove dispatch through the driver - suspense/component.rs: SuspenseDriver implements RenderDriver for suspense boundaries - Deleted any_props.rs Co-Authored-By: Evan Almloff --- packages/core/src/any_props.rs | 110 ----- packages/core/src/diff/component.rs | 85 +--- packages/core/src/lib.rs | 3 +- packages/core/src/nodes.rs | 31 +- packages/core/src/render_driver.rs | 265 +++++++++++ packages/core/src/scope_arena.rs | 39 +- packages/core/src/scope_context.rs | 39 +- packages/core/src/scopes.rs | 7 +- packages/core/src/suspense/component.rs | 569 +++++++++++++----------- packages/core/src/virtual_dom.rs | 35 +- 10 files changed, 681 insertions(+), 502 deletions(-) delete mode 100644 packages/core/src/any_props.rs create mode 100644 packages/core/src/render_driver.rs diff --git a/packages/core/src/any_props.rs b/packages/core/src/any_props.rs deleted file mode 100644 index 2ad9b5a3ab..0000000000 --- a/packages/core/src/any_props.rs +++ /dev/null @@ -1,110 +0,0 @@ -use crate::{ComponentFunction, Element, innerlude::CapturedPanic}; -use std::{any::Any, panic::AssertUnwindSafe}; - -pub(crate) type BoxedAnyProps = Box; - -/// A trait for a component that can be rendered. -pub(crate) trait AnyProps: 'static { - /// Render the component with the internal props. - fn render(&self) -> Element; - /// Make the old props equal to the new type erased props. Return if the props were equal and should be memoized. - fn memoize(&mut self, other: &dyn Any) -> bool; - /// Get the props as a type erased `dyn Any`. - fn props(&self) -> &dyn Any; - /// Get the props as a type erased `dyn Any`. - fn props_mut(&mut self) -> &mut dyn Any; - /// Duplicate this component into a new boxed component. - fn duplicate(&self) -> BoxedAnyProps; -} - -/// A component along with the props the component uses to render. -pub(crate) struct VProps, P, M> { - render_fn: F, - memo: fn(&mut P, &P) -> bool, - props: P, - name: &'static str, - phantom: std::marker::PhantomData, -} - -impl, P: Clone, M> Clone for VProps { - fn clone(&self) -> Self { - Self { - render_fn: self.render_fn.clone(), - memo: self.memo, - props: self.props.clone(), - name: self.name, - phantom: std::marker::PhantomData, - } - } -} - -impl + Clone, P: Clone + 'static, M: 'static> VProps { - /// Create a [`VProps`] object. - pub fn new( - render_fn: F, - memo: fn(&mut P, &P) -> bool, - props: P, - name: &'static str, - ) -> VProps { - VProps { - render_fn, - memo, - props, - name, - phantom: std::marker::PhantomData, - } - } -} - -impl + Clone, P: Clone + 'static, M: 'static> AnyProps - for VProps -{ - fn memoize(&mut self, other: &dyn Any) -> bool { - match other.downcast_ref::

() { - Some(other) => (self.memo)(&mut self.props, other), - None => false, - } - } - - fn props(&self) -> &dyn Any { - &self.props - } - - fn props_mut(&mut self) -> &mut dyn Any { - &mut self.props - } - - fn render(&self) -> Element { - fn render_inner(_name: &str, res: Result>) -> Element { - match res { - Ok(node) => node, - Err(err) => { - // on wasm this massively bloats binary sizes and we can't even capture the panic - // so do nothing - #[cfg(not(target_arch = "wasm32"))] - { - tracing::error!("Panic while rendering component `{_name}`: {err:?}"); - } - Element::Err(CapturedPanic(err).into()) - } - } - } - - render_inner( - self.name, - std::panic::catch_unwind(AssertUnwindSafe(move || { - self.render_fn.rebuild(self.props.clone()) - })), - ) - } - - fn duplicate(&self) -> BoxedAnyProps { - Box::new(Self { - render_fn: self.render_fn.clone(), - memo: self.memo, - props: self.props.clone(), - name: self.name, - phantom: std::marker::PhantomData, - }) - } -} diff --git a/packages/core/src/diff/component.rs b/packages/core/src/diff/component.rs index 4359f91ef6..054c17468a 100644 --- a/packages/core/src/diff/component.rs +++ b/packages/core/src/diff/component.rs @@ -1,18 +1,11 @@ -use std::{ - any::TypeId, - ops::{Deref, DerefMut}, -}; - use crate::{ - Element, SuspenseContext, - any_props::AnyProps, innerlude::{ - ElementRef, MountId, ScopeOrder, SuspenseBoundaryProps, SuspenseBoundaryPropsWithOwner, - VComponent, WriteMutations, + ElementRef, MountId, ScopeOrder, VComponent, WriteMutations, }, - nodes::VNode, + render_driver::DynWriter, scopes::{LastRenderedNode, ScopeId}, virtual_dom::VirtualDom, + nodes::VNode, }; impl VirtualDom { @@ -21,21 +14,16 @@ impl VirtualDom { to: Option<&mut M>, scope_id: ScopeId, ) { - let scope = &mut self.scopes[scope_id.0]; - if SuspenseBoundaryProps::downcast_from_props(&mut *scope.props).is_some() { - SuspenseBoundaryProps::diff(scope_id, self, to) - } else { - let new_nodes = self.run_scope(scope_id); - self.diff_scope(to, scope_id, new_nodes); - } + let driver = self.runtime.get_state(scope_id).render_driver(); + driver.diff(self, scope_id, DynWriter::erase(to)); } #[tracing::instrument(skip(self, to), level = "trace", name = "VirtualDom::diff_scope")] - fn diff_scope( + pub(crate) fn diff_scope( &mut self, to: Option<&mut M>, scope: ScopeId, - new_nodes: Element, + new_nodes: crate::Element, ) { self.runtime.clone().with_scope_on_stack(scope, || { // We don't diff the nodes if the scope is suspended or has an error @@ -98,18 +86,8 @@ impl VirtualDom { scope_id: ScopeId, replace_with: Option, ) { - // If this is a suspense boundary, remove the suspended nodes as well - SuspenseContext::remove_suspended_nodes::(self, scope_id, destroy_component_state); - - // Remove the component from the dom - if let Some(node) = self.scopes[scope_id.0].last_rendered_node.clone() { - node.remove_node_inner(self, to, destroy_component_state, replace_with) - }; - - if destroy_component_state { - // Now drop all the resources - self.drop_scope(scope_id); - } + let driver = self.runtime.get_state(scope_id).render_driver(); + driver.remove(self, scope_id, DynWriter::erase(to), destroy_component_state, replace_with); } } @@ -125,21 +103,17 @@ impl VNode { dom: &mut VirtualDom, to: Option<&mut impl WriteMutations>, ) { - // Replace components that have different render fns - if old.render_fn != new.render_fn { + // Replace components whose drivers identify different components + // (different driver type, or a different body function value) + if !old.driver.same_component(&*new.driver) { return self.replace_vcomponent(mount, idx, new, parent, dom, to); } - // copy out the box for both - let old_scope = &mut dom.scopes[scope_id.0]; - let old_props: &mut dyn AnyProps = old_scope.props.deref_mut(); - let new_props: &dyn AnyProps = new.props.deref(); - // If the props are static, then we try to memoize by setting the new with the old - // The target ScopeState still has the reference to the old props, so there's no need to update anything + // The scope's driver still owns the live props, so there's no need to update anything // This also implicitly drops the new props since they're not used - if old_props.memoize(new_props.props()) { - tracing::trace!("Memoized props for component {:#?}", scope_id,); + let scope_driver = dom.runtime.get_state(scope_id).render_driver(); + if scope_driver.memoize(new.driver.as_any()) { return; } @@ -181,37 +155,22 @@ impl VNode { dom: &mut VirtualDom, to: Option<&mut impl WriteMutations>, ) -> usize { - // If this is a suspense boundary, run our suspense creation logic instead of running the component - if component.props.props().type_id() == TypeId::of::() { - return SuspenseBoundaryProps::create(mount, idx, component, parent, dom, to); - } - let mut scope_id = ScopeId(dom.get_mounted_dyn_node(mount, idx)); + let new = scope_id.is_placeholder(); - // If the scopeid is a placeholder, we need to load up a new scope for this vcomponent. If it's already mounted, then we can just use that - if scope_id.is_placeholder() { + // If the scope id is a placeholder, we need to load up a new scope for this + // vcomponent. If it's already mounted, then we can just use that. + if new { scope_id = dom - .new_scope(component.props.duplicate(), component.name) + .new_scope(component.name, component.driver.duplicate()) .state() .id; // Store the scope id for the next render dom.set_mounted_dyn_node(mount, idx, scope_id.0); - - // If this is a new scope, we also need to run it once to get the initial state - let new = dom.run_scope(scope_id); - - // Then set the new node as the last rendered node - dom.scopes[scope_id.0].last_rendered_node = Some(LastRenderedNode::new(new)); } - let scope = ScopeId(dom.get_mounted_dyn_node(mount, idx)); - - let new_node = dom.scopes[scope.0] - .last_rendered_node - .clone() - .expect("Component to be mounted"); - - dom.create_scope(to, scope, new_node, parent) + let driver = dom.runtime.get_state(scope_id).render_driver(); + driver.create(dom, scope_id, new, parent, DynWriter::erase(to)) } } diff --git a/packages/core/src/lib.rs b/packages/core/src/lib.rs index 2abc70dfa0..f236003d1d 100644 --- a/packages/core/src/lib.rs +++ b/packages/core/src/lib.rs @@ -3,7 +3,6 @@ #![doc(html_favicon_url = "https://avatars.githubusercontent.com/u/79236386")] #![warn(missing_docs)] -mod any_props; mod arena; mod diff; mod effect; @@ -18,6 +17,7 @@ mod nodes; mod properties; mod reactive_context; mod render_error; +pub(crate) mod render_driver; mod root_wrapper; mod runtime; mod scheduler; @@ -54,7 +54,6 @@ pub mod internal { } pub(crate) mod innerlude { - pub(crate) use crate::any_props::*; pub use crate::arena::*; pub(crate) use crate::effect::*; pub use crate::error_boundary::*; diff --git a/packages/core/src/nodes.rs b/packages/core/src/nodes.rs index 10e673545b..4052fc1119 100644 --- a/packages/core/src/nodes.rs +++ b/packages/core/src/nodes.rs @@ -1,14 +1,15 @@ +use std::rc::Rc; + use crate::{ Element, Event, Properties, ScopeId, VirtualDom, - any_props::BoxedAnyProps, arena::ElementId, events::ListenerCallback, - innerlude::{ElementRef, MountId, ScopeState, VProps}, + innerlude::{ElementRef, MountId, ScopeState}, properties::ComponentFunction, + render_driver::{BodyDriver, RenderDriver}, }; use dioxus_core_types::DioxusFormattable; use std::ops::Deref; -use std::rc::Rc; use std::vec; use std::{ any::{Any, TypeId}, @@ -631,19 +632,15 @@ pub struct VComponent { /// The name of this component pub name: &'static str, - /// The raw pointer to the render function - pub(crate) render_fn: usize, - - /// The props for this component - pub(crate) props: BoxedAnyProps, + /// The driver owning this component's rendering lifecycle and props. + pub(crate) driver: Rc, } impl Clone for VComponent { fn clone(&self) -> Self { Self { name: self.name, - props: self.props.duplicate(), - render_fn: self.render_fn, + driver: self.driver.duplicate(), } } } @@ -658,8 +655,7 @@ impl VComponent { where P: Properties + 'static, { - let render_fn = component.fn_ptr(); - let props = Box::new(VProps::new( + let driver = Rc::new(BodyDriver::new( component,

::memoize, props, @@ -667,12 +663,19 @@ impl VComponent { )); VComponent { - render_fn, name: fn_name, - props, + driver, } } + /// Create a [`VComponent`] with a custom [`RenderDriver`]. + pub(crate) fn new_with_driver( + name: &'static str, + driver: Rc, + ) -> Self { + VComponent { name, driver } + } + /// Get the [`ScopeId`] this node is mounted to if it's mounted /// /// This is useful for rendering nodes outside of the VirtualDom, such as in SSR diff --git a/packages/core/src/render_driver.rs b/packages/core/src/render_driver.rs new file mode 100644 index 0000000000..22960aad13 --- /dev/null +++ b/packages/core/src/render_driver.rs @@ -0,0 +1,265 @@ +use std::{any::Any, cell::RefCell, panic::AssertUnwindSafe, rc::Rc}; + +use crate::{ + AttributeValue, ComponentFunction, Element, Template, WriteMutations, + arena::ElementId, + innerlude::{CapturedPanic, ElementRef, ScopeOrder}, + scopes::{LastRenderedNode, ScopeId}, + virtual_dom::VirtualDom, +}; + +/// A sized wrapper around `&mut dyn WriteMutations` that itself implements +/// `WriteMutations`, letting the `dyn RenderDriver` layer bridge into the +/// generic (`M: WriteMutations + Sized`) diffing methods without requiring +/// `?Sized` bounds throughout the diff pipeline. +pub(crate) struct DynWriter<'a>(&'a mut dyn WriteMutations); + +impl<'a> DynWriter<'a> { + /// Erase a generic `Option<&mut M>` into `Option<&mut DynWriter>`. + /// + /// The returned option borrows the original writer through the `DynWriter` + /// wrapper, so the caller must not use the original `to` while the wrapper + /// is alive. + #[inline] + pub fn erase(to: Option<&mut M>) -> Option> { + to.map(|m| DynWriter(m as &mut dyn WriteMutations)) + } +} + +impl WriteMutations for DynWriter<'_> { + fn append_children(&mut self, id: ElementId, m: usize) { self.0.append_children(id, m) } + fn assign_node_id(&mut self, path: &'static [u8], id: ElementId) { self.0.assign_node_id(path, id) } + fn create_placeholder(&mut self, id: ElementId) { self.0.create_placeholder(id) } + fn create_text_node(&mut self, value: &str, id: ElementId) { self.0.create_text_node(value, id) } + fn load_template(&mut self, template: Template, index: usize, id: ElementId) { self.0.load_template(template, index, id) } + fn replace_node_with(&mut self, id: ElementId, m: usize) { self.0.replace_node_with(id, m) } + fn replace_placeholder_with_nodes(&mut self, path: &'static [u8], m: usize) { self.0.replace_placeholder_with_nodes(path, m) } + fn insert_nodes_after(&mut self, id: ElementId, m: usize) { self.0.insert_nodes_after(id, m) } + fn insert_nodes_before(&mut self, id: ElementId, m: usize) { self.0.insert_nodes_before(id, m) } + fn set_attribute(&mut self, name: &'static str, ns: Option<&'static str>, value: &AttributeValue, id: ElementId) { self.0.set_attribute(name, ns, value, id) } + fn set_node_text(&mut self, value: &str, id: ElementId) { self.0.set_node_text(value, id) } + fn create_event_listener(&mut self, name: &'static str, id: ElementId) { self.0.create_event_listener(name, id) } + fn remove_event_listener(&mut self, name: &'static str, id: ElementId) { self.0.remove_event_listener(name, id) } + fn remove_node(&mut self, id: ElementId) { self.0.remove_node(id) } + fn push_root(&mut self, id: ElementId) { self.0.push_root(id) } +} + +/// A scope's rendering lifecycle and the inputs it renders from. +/// +/// Every scope owns exactly one driver, attached when its [`VComponent`] is +/// constructed and fixed for the scope's lifetime: plain components use +/// [`BodyDriver`], which owns the component function and its props and +/// mounts/diffs the element the body returns, while suspense components attach +/// drivers in their `into_vcomponent` that own their props and manage the +/// scope's `last_rendered_node` directly, with no body to run. +/// +/// A driver instance is per component instance: the scope adopts a +/// [`Self::duplicate`] of the vnode's driver at creation so the live scope +/// never aliases inputs with a vnode, and [`Self::memoize`] is how a parent +/// render hands the scope its new inputs. +pub(crate) trait RenderDriver: 'static { + /// The driver as `Any`, for [`Self::memoize`] hand-offs between two + /// instances of the same driver type. + fn as_any(&self) -> &dyn Any; + + /// Whether `other` renders the same component as this driver, i.e. a + /// scope rendered by this driver can be diffed in place against + /// `other`'s props rather than replaced. Two drivers of one type + /// identify the same component by default; [`BodyDriver`] also compares + /// its function value, since dynamic components can put different + /// functions of one type in a slot. + fn same_component(&self, other: &dyn RenderDriver) -> bool { + self.as_any().type_id() == other.as_any().type_id() + } + + /// Make this driver's props equal to `new_driver`'s (a driver of the + /// same concrete type, guaranteed by the [`Self::same_component`] + /// check). Returns whether the props were equal and the scope can be + /// memoized. + fn memoize(&self, new_driver: &dyn Any) -> bool; + + /// A fresh driver instance with cloned props, for [`VComponent`] clones + /// and scope adoption. + /// + /// [`VComponent`]: crate::nodes::VComponent + fn duplicate(&self) -> Rc; + + /// Mount this scope's output. `new` is true when the scope was allocated + /// for this create and has never run or rendered. + fn create( + &self, + dom: &mut VirtualDom, + scope_id: ScopeId, + new: bool, + parent: Option, + to: Option>, + ) -> usize; + + /// Diff this scope's output against its current props. + fn diff( + &self, + dom: &mut VirtualDom, + scope_id: ScopeId, + to: Option>, + ); + + /// Remove this scope's output. When `destroy_component_state` is false + /// the output is only being lifted out of the real DOM and the driver + /// must keep component state alive. + fn remove( + &self, + dom: &mut VirtualDom, + scope_id: ScopeId, + to: Option>, + destroy_component_state: bool, + replace_with: Option, + ); +} + +/// Remove a scope's rendered output from the DOM, and drop the scope when +/// `destroy_component_state` is set. Shared by [`BodyDriver`] and drivers +/// whose output is removed the same way (suspense). +pub(crate) fn remove_rendered_output( + dom: &mut VirtualDom, + scope_id: ScopeId, + to: Option<&mut M>, + destroy_component_state: bool, + replace_with: Option, +) { + if let Some(node) = dom.scopes[scope_id.0].last_rendered_node.clone() { + node.remove_node_inner(dom, to, destroy_component_state, replace_with) + }; + + if destroy_component_state { + dom.drop_scope(scope_id); + } +} + +/// The rendering lifecycle of a plain component: the driver owns the +/// component function and its props, runs the body, and the element it +/// returns is the scope's rendered output. +pub(crate) struct BodyDriver, P, M> { + render_fn: F, + memo: fn(&mut P, &P) -> bool, + props: RefCell

, + name: &'static str, + phantom: std::marker::PhantomData, +} + +impl + Clone, P: Clone + 'static, M: 'static> BodyDriver { + pub fn new( + render_fn: F, + memo: fn(&mut P, &P) -> bool, + props: P, + name: &'static str, + ) -> BodyDriver { + BodyDriver { + render_fn, + memo, + props: RefCell::new(props), + name, + phantom: std::marker::PhantomData, + } + } + + fn render(&self) -> Element { + fn render_inner(_name: &str, res: Result>) -> Element { + match res { + Ok(node) => node, + Err(err) => { + #[cfg(not(target_arch = "wasm32"))] + { + tracing::error!("Panic while rendering component `{_name}`: {err:?}"); + } + Element::Err(CapturedPanic(err).into()) + } + } + } + + let props = self.props.borrow().clone(); + render_inner( + self.name, + std::panic::catch_unwind(AssertUnwindSafe(move || self.render_fn.rebuild(props))), + ) + } +} + +impl + Clone, P: Clone + 'static, M: 'static> RenderDriver + for BodyDriver +{ + fn as_any(&self) -> &dyn Any { + self + } + + fn same_component(&self, other: &dyn RenderDriver) -> bool { + other + .as_any() + .downcast_ref::() + .is_some_and(|other| other.render_fn.fn_ptr() == self.render_fn.fn_ptr()) + } + + fn memoize(&self, new_driver: &dyn Any) -> bool { + match new_driver.downcast_ref::() { + Some(new) => (self.memo)(&mut self.props.borrow_mut(), &new.props.borrow()), + None => false, + } + } + + fn duplicate(&self) -> Rc { + Rc::new(Self { + render_fn: self.render_fn.clone(), + memo: self.memo, + props: RefCell::new(self.props.borrow().clone()), + name: self.name, + phantom: std::marker::PhantomData, + }) + } + + fn create( + &self, + dom: &mut VirtualDom, + scope_id: ScopeId, + new: bool, + parent: Option, + mut to: Option>, + ) -> usize { + if new { + let body = dom.run_scope_with(scope_id, || self.render()); + dom.scopes[scope_id.0].last_rendered_node = Some(LastRenderedNode::new(body)); + } + + // If our scope landed in `dirty_scopes` during its initial render + // (e.g. a hook synchronously queued an update for itself), drain the + // entry now so we don't re-process the same scope after creation. + let height = dom.runtime.get_state(scope_id).height; + dom.dirty_scopes.remove(&ScopeOrder::new(height, scope_id)); + + let new_node = dom.scopes[scope_id.0] + .last_rendered_node + .clone() + .expect("Component to be mounted"); + + dom.create_scope(to.as_mut(), scope_id, new_node, parent) + } + + fn diff( + &self, + dom: &mut VirtualDom, + scope_id: ScopeId, + mut to: Option>, + ) { + let body = dom.run_scope_with(scope_id, || self.render()); + dom.diff_scope(to.as_mut(), scope_id, body); + } + + fn remove( + &self, + dom: &mut VirtualDom, + scope_id: ScopeId, + mut to: Option>, + destroy_component_state: bool, + replace_with: Option, + ) { + remove_rendered_output(dom, scope_id, to.as_mut(), destroy_component_state, replace_with); + } +} diff --git a/packages/core/src/scope_arena.rs b/packages/core/src/scope_arena.rs index a23113ed68..3a48c543c4 100644 --- a/packages/core/src/scope_arena.rs +++ b/packages/core/src/scope_arena.rs @@ -1,17 +1,22 @@ +use std::rc::Rc; + use crate::{ Element, ReactiveContext, - any_props::{AnyProps, BoxedAnyProps}, innerlude::{RenderError, ScopeOrder, ScopeState}, + render_driver::RenderDriver, scope_context::{Scope, SuspenseLocation}, scopes::ScopeId, virtual_dom::VirtualDom, }; impl VirtualDom { + /// Create a scope rendering into the current scope's render target (the + /// root target when no scope is active). `driver` owns the scope's + /// rendering lifecycle and props. pub(super) fn new_scope( &mut self, - props: BoxedAnyProps, name: &'static str, + driver: Rc, ) -> &mut ScopeState { let parent_id = self.runtime.try_current_scope_id(); let height = match parent_id.and_then(|id| self.runtime.try_get_state(id)) { @@ -25,13 +30,12 @@ impl VirtualDom { let entry = self.scopes.vacant_entry(); let id = ScopeId(entry.key()); - let scope_runtime = Scope::new(name, id, parent_id, height, suspense_boundary); + let scope_runtime = Scope::new(name, id, parent_id, height, suspense_boundary, driver); let reactive_context = ReactiveContext::new_for_scope(&scope_runtime, &self.runtime); let scope = entry.insert(ScopeState { runtime: self.runtime.clone(), context_id: id, - props, last_rendered_node: Default::default(), reactive_context, }); @@ -41,10 +45,15 @@ impl VirtualDom { scope } - /// Run a scope and return the rendered nodes. This will not modify the DOM or update the last rendered node of the scope. - #[tracing::instrument(skip(self), level = "trace", name = "VirtualDom::run_scope")] + /// Run a scope's body via `render` and return the rendered nodes. This + /// will not modify the DOM or update the last rendered node of the scope. + #[tracing::instrument(skip(self, render), level = "trace", name = "VirtualDom::run_scope")] #[track_caller] - pub(crate) fn run_scope(&mut self, scope_id: ScopeId) -> Element { + pub(crate) fn run_scope_with( + &mut self, + scope_id: ScopeId, + render: impl FnOnce() -> Element, + ) -> Element { // Ensure we are currently inside a `Runtime`. crate::Runtime::current(); @@ -60,12 +69,10 @@ impl VirtualDom { pre_run(); } - let props: &dyn AnyProps = &*scope.props; - let span = tracing::trace_span!("render", scope = %scope.state().name); span.in_scope(|| { scope.reactive_context.reset_and_run_in(|| { - let render_return = props.render(); + let render_return = render(); // After the component is run, we need to do a deep clone of the VNode. This // breaks any references to mounted parts of the VNode from the component. // Without this, the component could store a mounted version of the VNode @@ -90,16 +97,18 @@ impl VirtualDom { }) }; - let scope_state = scope.state(); + { + let scope_state = scope.state(); - // Run all post-render hooks - for post_run in scope_state.after_render.borrow_mut().iter_mut() { - post_run(); + // Run all post-render hooks + for post_run in scope_state.after_render.borrow_mut().iter_mut() { + post_run(); + } } // remove this scope from dirty scopes self.dirty_scopes - .remove(&ScopeOrder::new(scope_state.height, scope_id)); + .remove(&ScopeOrder::new(scope.state().height, scope_id)); output }) } diff --git a/packages/core/src/scope_context.rs b/packages/core/src/scope_context.rs index 603643b38c..1ce5494fdd 100644 --- a/packages/core/src/scope_context.rs +++ b/packages/core/src/scope_context.rs @@ -1,6 +1,7 @@ use crate::{ Runtime, ScopeId, Task, innerlude::{SchedulerMsg, SuspenseContext}, + render_driver::RenderDriver, }; use generational_box::{AnyStorage, Owner}; use rustc_hash::FxHashSet; @@ -8,6 +9,7 @@ use std::{ any::Any, cell::{Cell, RefCell}, future::Future, + rc::Rc, sync::Arc, }; @@ -23,7 +25,6 @@ pub(crate) enum ScopeStatus { pub(crate) enum SuspenseLocation { #[default] NotSuspended, - SuspenseBoundary(SuspenseContext), UnderSuspense(SuspenseContext), InSuspensePlaceholder(SuspenseContext), } @@ -33,7 +34,6 @@ impl SuspenseLocation { match self { SuspenseLocation::InSuspensePlaceholder(context) => Some(context), SuspenseLocation::UnderSuspense(context) => Some(context), - SuspenseLocation::SuspenseBoundary(context) => Some(context), _ => None, } } @@ -57,8 +57,14 @@ pub(crate) struct Scope { pub(crate) before_render: RefCell>>, pub(crate) after_render: RefCell>>, - /// The suspense boundary that this scope is currently in (if any) - suspense_boundary: SuspenseLocation, + /// The suspense boundary location this scope is rendered under, if any. + suspense_location: SuspenseLocation, + + /// The suspense context owned by this scope when this scope is a boundary. + suspense_boundary: RefCell>, + + /// The driver owning this scope's rendered output. + render_driver: Rc, pub(crate) status: RefCell, } @@ -69,7 +75,8 @@ impl Scope { id: ScopeId, parent_id: Option, height: u32, - suspense_boundary: SuspenseLocation, + suspense_location: SuspenseLocation, + render_driver: Rc, ) -> Self { Self { name, @@ -86,7 +93,9 @@ impl Scope { status: RefCell::new(ScopeStatus::Unmounted { effects_queued: Vec::new(), }), - suspense_boundary, + suspense_location, + suspense_boundary: RefCell::new(None), + render_driver, } } @@ -94,6 +103,15 @@ impl Scope { self.parent_id } + /// The driver owning this scope's rendered output. + pub(crate) fn render_driver(&self) -> Rc { + self.render_driver.clone() + } + + pub(crate) fn set_suspense_boundary(&self, context: SuspenseContext) { + self.suspense_boundary.replace(Some(context)); + } + fn sender(&self) -> futures_channel::mpsc::UnboundedSender { Runtime::current().sender.clone() } @@ -111,20 +129,17 @@ impl Scope { /// Get the suspense location of this scope pub(crate) fn suspense_location(&self) -> SuspenseLocation { - self.suspense_boundary.clone() + self.suspense_location.clone() } /// If this scope is a suspense boundary, return the suspense context pub(crate) fn suspense_boundary(&self) -> Option { - match self.suspense_location() { - SuspenseLocation::SuspenseBoundary(context) => Some(context), - _ => None, - } + self.suspense_boundary.borrow().clone() } /// Check if a node should run during suspense pub(crate) fn should_run_during_suspense(&self) -> bool { - let Some(context) = self.suspense_boundary.suspense_context() else { + let Some(context) = self.suspense_location.suspense_context() else { return false; }; diff --git a/packages/core/src/scopes.rs b/packages/core/src/scopes.rs index 014148cef7..798ce24003 100644 --- a/packages/core/src/scopes.rs +++ b/packages/core/src/scopes.rs @@ -1,5 +1,5 @@ use crate::{ - Element, RenderError, Runtime, VNode, any_props::BoxedAnyProps, + Element, RenderError, Runtime, VNode, reactive_context::ReactiveContext, scope_context::Scope, }; use std::{cell::Ref, rc::Rc}; @@ -73,10 +73,9 @@ impl ScopeId { pub struct ScopeState { pub(crate) runtime: Rc, pub(crate) context_id: ScopeId, - /// The last node that has been rendered for this component. This node may not ben mounted - /// During suspense, this component can be rendered in the background multiple times + /// The last node that has been rendered for this component. This node may not be mounted. + /// During suspense, this component can be rendered in the background multiple times. pub(crate) last_rendered_node: Option, - pub(crate) props: BoxedAnyProps, pub(crate) reactive_context: ReactiveContext, } diff --git a/packages/core/src/suspense/component.rs b/packages/core/src/suspense/component.rs index d4662da6e8..df080c0e96 100644 --- a/packages/core/src/suspense/component.rs +++ b/packages/core/src/suspense/component.rs @@ -1,4 +1,10 @@ -use crate::{innerlude::*, scope_context::SuspenseLocation}; +use std::{any::Any, cell::RefCell, rc::Rc}; + +use crate::{ + innerlude::*, + render_driver::{DynWriter, RenderDriver, remove_rendered_output}, + scope_context::SuspenseLocation, +}; /// Properties for the [`SuspenseBoundary()`] component. #[allow(non_camel_case_types)] @@ -49,13 +55,9 @@ where SuspenseBoundaryProps::builder() } fn memoize(&mut self, new: &Self) -> bool { - let equal = self == new; self.fallback.__point_to(&new.fallback); - if !equal { - let new_clone = new.clone(); - self.children = new_clone.children; - } - equal + self.children = new.children.clone(); + false } } #[doc(hidden)] @@ -177,14 +179,10 @@ impl SuspenseBoundaryPropsWithOwner { /// Create a component from the props. pub fn into_vcomponent( self, - render_fn: impl ComponentFunction, + _render_fn: impl ComponentFunction, ) -> VComponent { - let component_name = std::any::type_name_of_val(&render_fn); - VComponent::new( - move |wrapper: Self| render_fn.rebuild(wrapper.inner), - self, - component_name, - ) + let component_name = std::any::type_name_of_val(&_render_fn); + VComponent::new_with_driver(component_name, Rc::new(SuspenseDriver::new(self))) } } impl Properties for SuspenseBoundaryPropsWithOwner { @@ -239,9 +237,110 @@ impl ::core::cmp::PartialEq for SuspenseBoundaryProps { /// } /// ``` #[allow(non_snake_case)] -pub fn SuspenseBoundary(mut __props: SuspenseBoundaryProps) -> Element { +#[cfg_attr(coverage_nightly, coverage(off))] +pub fn SuspenseBoundary(__props: SuspenseBoundaryProps) -> Element { unreachable!("SuspenseBoundary should not be called directly") } + +/// The rendering lifecycle of a suspense boundary scope: children render in +/// the background first, and the scope's output is either the children or +/// the fallback depending on whether any descendant suspended. +struct SuspenseDriver { + props: RefCell, +} + +impl SuspenseDriver { + fn new(props: SuspenseBoundaryPropsWithOwner) -> Self { + Self { + props: RefCell::new(props), + } + } + + fn children(&self) -> LastRenderedNode { + self.props.borrow().inner.children.clone() + } + + fn fallback(&self) -> Callback { + self.props.borrow().inner.fallback + } + + fn store_children(&self, children: &LastRenderedNode) { + self.props.borrow_mut().inner.children.clone_from(children); + } +} + +/// The suspense driver owning `scope_id`, which must be a suspense boundary +/// scope. +fn suspense_driver(dom: &VirtualDom, scope_id: ScopeId) -> Rc { + dom.runtime.get_state(scope_id).render_driver() +} + +fn as_suspense(driver: &Rc) -> &SuspenseDriver { + driver + .as_any() + .downcast_ref::() + .expect("suspense boundary scopes carry a SuspenseDriver") +} + +impl RenderDriver for SuspenseDriver { + fn as_any(&self) -> &dyn Any { + self + } + + fn memoize(&self, new_driver: &dyn Any) -> bool { + match new_driver.downcast_ref::() { + Some(new) => Properties::memoize(&mut *self.props.borrow_mut(), &new.props.borrow()), + None => false, + } + } + + fn duplicate(&self) -> Rc { + Rc::new(Self::new(self.props.borrow().clone())) + } + + fn create( + &self, + dom: &mut VirtualDom, + scope_id: ScopeId, + new: bool, + parent: Option, + to: Option>, + ) -> usize { + if new { + let suspense_context = SuspenseContext::new(); + let scope_state = dom.runtime.get_state(scope_id); + scope_state.set_suspense_boundary(suspense_context.clone()); + suspense_context.mount(scope_id); + } + suspense_create(self, scope_id, parent, dom, to) + } + + fn diff( + &self, + dom: &mut VirtualDom, + scope_id: ScopeId, + to: Option>, + ) { + suspense_diff(self, scope_id, dom, to) + } + + fn remove( + &self, + dom: &mut VirtualDom, + scope_id: ScopeId, + mut to: Option>, + destroy_component_state: bool, + replace_with: Option, + ) { + // If this is a suspense boundary, remove the suspended nodes as well + SuspenseContext::remove_suspended_nodes(dom, scope_id, destroy_component_state); + + // The scope's rendered output (children or fallback) is removed the + // same way a plain component's output is. + remove_rendered_output(dom, scope_id, to.as_mut(), destroy_component_state, replace_with); + } +} + #[allow(non_snake_case)] #[doc(hidden)] mod SuspenseBoundary_completions { @@ -256,115 +355,57 @@ mod SuspenseBoundary_completions { pub use SuspenseBoundary_completions::Component::SuspenseBoundary; use generational_box::Owner; -/// Suspense has a custom diffing algorithm that diffs the suspended nodes in the background without rendering them -impl SuspenseBoundaryProps { - /// Try to downcast [`AnyProps`] to [`SuspenseBoundaryProps`] - pub(crate) fn downcast_from_props(props: &mut dyn AnyProps) -> Option<&mut Self> { - let inner: Option<&mut SuspenseBoundaryPropsWithOwner> = props.props_mut().downcast_mut(); - inner.map(|inner| &mut inner.inner) - } - - pub(crate) fn create( - mount: MountId, - idx: usize, - component: &VComponent, - parent: Option, - dom: &mut VirtualDom, - to: Option<&mut M>, - ) -> usize { - let mut scope_id = ScopeId(dom.get_mounted_dyn_node(mount, idx)); - // If the ScopeId is a placeholder, we need to load up a new scope for this vcomponent. If it's already mounted, then we can just use that - if scope_id.is_placeholder() { - { - let suspense_context = SuspenseContext::new(); - - let suspense_boundary_location = - crate::scope_context::SuspenseLocation::SuspenseBoundary( - suspense_context.clone(), - ); - dom.runtime - .clone() - .with_suspense_location(suspense_boundary_location, || { - let scope_state = dom - .new_scope(component.props.duplicate(), component.name) - .state(); - suspense_context.mount(scope_state.id); - scope_id = scope_state.id; - }); - } - - // Store the scope id for the next render - dom.set_mounted_dyn_node(mount, idx, scope_id.0); +/// Mount a suspense boundary scope: render the children in the background +/// first, then mount either the children or the fallback depending on whether +/// anything suspended. +fn suspense_create( + driver: &SuspenseDriver, + scope_id: ScopeId, + parent: Option, + dom: &mut VirtualDom, + mut to: Option>, +) -> usize { + dom.runtime.clone().with_scope_on_stack(scope_id, || { + let suspense_context = dom.runtime.get_state(scope_id).suspense_boundary().unwrap(); + + let children = driver.children(); + + // First always render the children in the background. Rendering the children may cause this boundary to suspend + suspense_context.under_suspense_boundary(&dom.runtime(), || { + children.create(dom, parent, Option::<&mut NoOpMutations>::None); + }); + + // Store the (now mounted) children back + driver.store_children(&children); + + // If there are suspended futures, render the fallback + if !suspense_context.suspended_futures().is_empty() { + let (node, nodes_created) = + suspense_context.in_suspense_placeholder(&dom.runtime(), || { + suspense_context.set_suspended_nodes(children.as_vnode().clone()); + let suspense_placeholder = + LastRenderedNode::new(driver.fallback().call(suspense_context.clone())); + let nodes_created = suspense_placeholder.create(dom, parent, to.as_mut()); + (suspense_placeholder, nodes_created) + }); + + dom.scopes[scope_id.0].last_rendered_node = Some(node); + nodes_created + } else { + // Otherwise just render the children in the real dom + debug_assert!(children.mount.get().mounted()); + let nodes_created = suspense_context + .under_suspense_boundary(&dom.runtime(), || children.create(dom, parent, to.as_mut())); + dom.scopes[scope_id.0].last_rendered_node = children.into(); + suspense_context.take_suspended_nodes(); + mark_suspense_resolved(&suspense_context, dom, scope_id); + + nodes_created } - dom.runtime.clone().with_scope_on_stack(scope_id, || { - let scope_state = &mut dom.scopes[scope_id.0]; - let props = Self::downcast_from_props(&mut *scope_state.props).unwrap(); - let suspense_context = - SuspenseContext::downcast_suspense_boundary_from_scope(&dom.runtime, scope_id) - .unwrap(); - - let children = props.children.clone(); - - // First always render the children in the background. Rendering the children may cause this boundary to suspend - suspense_context.under_suspense_boundary(&dom.runtime(), || { - children.create(dom, parent, None::<&mut M>); - }); - - // Store the (now mounted) children back into the scope state - let scope_state = &mut dom.scopes[scope_id.0]; - let props = Self::downcast_from_props(&mut *scope_state.props).unwrap(); - props.children.clone_from(&children); - - let scope_state = &mut dom.scopes[scope_id.0]; - let suspense_context = scope_state - .state() - .suspense_location() - .suspense_context() - .unwrap() - .clone(); - - // If there are suspended futures, render the fallback - - if !suspense_context.suspended_futures().is_empty() { - let (node, nodes_created) = - suspense_context.in_suspense_placeholder(&dom.runtime(), || { - let scope_state = &mut dom.scopes[scope_id.0]; - let props = Self::downcast_from_props(&mut *scope_state.props).unwrap(); - let suspense_context = - SuspenseContext::downcast_suspense_boundary_from_scope( - &dom.runtime, - scope_id, - ) - .unwrap(); - suspense_context.set_suspended_nodes(children.as_vnode().clone()); - let suspense_placeholder = - LastRenderedNode::new(props.fallback.call(suspense_context)); - let nodes_created = suspense_placeholder.create(dom, parent, to); - (suspense_placeholder, nodes_created) - }); - - let scope_state = &mut dom.scopes[scope_id.0]; - scope_state.last_rendered_node = Some(node); - - nodes_created - } else { - // Otherwise just render the children in the real dom - debug_assert!(children.mount.get().mounted()); - let nodes_created = suspense_context - .under_suspense_boundary(&dom.runtime(), || children.create(dom, parent, to)); - let scope_state = &mut dom.scopes[scope_id.0]; - scope_state.last_rendered_node = children.into(); - let suspense_context = - SuspenseContext::downcast_suspense_boundary_from_scope(&dom.runtime, scope_id) - .unwrap(); - suspense_context.take_suspended_nodes(); - mark_suspense_resolved(&suspense_context, dom, scope_id); - - nodes_created - } - }) - } + }) +} +impl SuspenseBoundaryProps { #[doc(hidden)] /// Manually rerun the children of this suspense boundary without diffing against the old nodes. /// @@ -383,12 +424,7 @@ impl SuspenseBoundaryProps { }; // Reset the suspense context - let suspense_context = scope_state - .state() - .suspense_location() - .suspense_context() - .unwrap() - .clone(); + let suspense_context = scope_state.state().suspense_boundary().unwrap().clone(); suspense_context.inner.suspended_tasks.borrow_mut().clear(); // Get the parent of the suspense boundary to later create children with the right parent @@ -402,10 +438,11 @@ impl SuspenseBoundaryProps { .parent }; - let props = Self::downcast_from_props(&mut *scope_state.props).unwrap(); + let driver = suspense_driver(dom, scope_id); + let driver = as_suspense(&driver); // Unmount any children to reset any scopes under this suspense boundary - let children = props.children.clone(); + let children = driver.children(); let suspense_context = SuspenseContext::downcast_suspense_boundary_from_scope(&dom.runtime, scope_id) .unwrap(); @@ -413,7 +450,7 @@ impl SuspenseBoundaryProps { // Take the suspended nodes out of the suspense boundary so the children know that the boundary is not suspended while diffing let suspended = suspense_context.take_suspended_nodes(); if let Some(node) = suspended { - node.remove_node(&mut *dom, None::<&mut M>, None); + node.remove_node(&mut *dom, Option::<&mut NoOpMutations>::None, None); } // Replace the rendered nodes with resolved nodes @@ -429,149 +466,156 @@ impl SuspenseBoundaryProps { children.create(dom, parent, Some(to)); }); - // Store the (now mounted) children back into the scope state - let scope_state = &mut dom.scopes[scope_id.0]; - let props = Self::downcast_from_props(&mut *scope_state.props).unwrap(); - props.children.clone_from(&children); - scope_state.last_rendered_node = Some(children); + // Store the (now mounted) children back + driver.store_children(&children); + dom.scopes[scope_id.0].last_rendered_node = Some(children); // Run any closures that were waiting for the suspense to resolve suspense_context.run_resolved_closures(&dom.runtime); }) } +} - pub(crate) fn diff( - scope_id: ScopeId, - dom: &mut VirtualDom, - to: Option<&mut M>, - ) { - dom.runtime.clone().with_scope_on_stack(scope_id, || { - let scope = &mut dom.scopes[scope_id.0]; - let myself = Self::downcast_from_props(&mut *scope.props) - .unwrap() - .clone(); - - let last_rendered_node = scope.last_rendered_node.clone().unwrap(); - - let Self { - fallback, children, .. - } = myself; - - let suspense_context = scope.state().suspense_boundary().unwrap().clone(); - let suspended_nodes = suspense_context.suspended_nodes(); - let suspended = !suspense_context.suspended_futures().is_empty(); - match (suspended_nodes, suspended) { - // We already have suspended nodes that still need to be suspended - // Just diff the normal and suspended nodes - (Some(suspended_nodes), true) => { - let new_suspended_nodes: VNode = children.as_vnode().clone(); - - // Diff the placeholder nodes in the dom - let new_placeholder = - suspense_context.in_suspense_placeholder(&dom.runtime(), || { - let old_placeholder = last_rendered_node; - let new_placeholder = - LastRenderedNode::new(fallback.call(suspense_context.clone())); - - old_placeholder.diff_node(&new_placeholder, dom, to); - new_placeholder - }); - - // Set the last rendered node to the placeholder - dom.scopes[scope_id.0].last_rendered_node = Some(new_placeholder); - - // Diff the suspended nodes in the background - suspense_context.under_suspense_boundary(&dom.runtime(), || { - suspended_nodes.diff_node(&new_suspended_nodes, dom, None::<&mut M>); - }); +/// Diff a suspense boundary scope against its current children/fallback props. +fn suspense_diff( + driver: &SuspenseDriver, + scope_id: ScopeId, + dom: &mut VirtualDom, + mut to: Option>, +) { + dom.runtime.clone().with_scope_on_stack(scope_id, || { + let scope = &mut dom.scopes[scope_id.0]; + let last_rendered_node = scope.last_rendered_node.clone().unwrap(); + + let children = driver.children(); + let fallback = driver.fallback(); + + let suspense_context = scope.state().suspense_boundary().unwrap().clone(); + let suspended_nodes = suspense_context.suspended_nodes(); + let suspended = !suspense_context.suspended_futures().is_empty(); + match (suspended_nodes, suspended) { + // We already have suspended nodes that still need to be suspended + // Just diff the normal and suspended nodes + (Some(suspended_nodes), true) => { + let new_suspended_nodes: VNode = children.as_vnode().clone(); + + // Diff the placeholder nodes in the dom + let new_placeholder = + suspense_context.in_suspense_placeholder(&dom.runtime(), || { + let new_placeholder = + LastRenderedNode::new(fallback.call(suspense_context.clone())); - let suspense_context = SuspenseContext::downcast_suspense_boundary_from_scope( - &dom.runtime, - scope_id, - ) - .unwrap(); - suspense_context.set_suspended_nodes(new_suspended_nodes); - } - // We have no suspended nodes, and we are not suspended. Just diff the children like normal - (None, false) => { - let old_children = last_rendered_node; - let new_children = children; - - suspense_context.under_suspense_boundary(&dom.runtime(), || { - old_children.diff_node(&new_children, dom, to); + last_rendered_node.diff_node(&new_placeholder, dom, to.as_mut()); + new_placeholder }); - // Set the last rendered node to the new children - dom.scopes[scope_id.0].last_rendered_node = new_children.into(); - } - // We have no suspended nodes, but we just became suspended. Move the children to the background - (None, true) => { - let old_children = last_rendered_node; - let new_children: VNode = children.as_vnode().clone(); + // Set the last rendered node to the placeholder + dom.scopes[scope_id.0].last_rendered_node = Some(new_placeholder); - let new_placeholder = - LastRenderedNode::new(fallback.call(suspense_context.clone())); + // Diff the suspended nodes in the background + suspense_context.under_suspense_boundary(&dom.runtime(), || { + suspended_nodes.diff_node( + &new_suspended_nodes, + dom, + Option::<&mut NoOpMutations>::None, + ); + }); - // Move the children to the background - let mount = old_children.mount.get(); - let parent = dom.get_mounted_parent(mount); + let suspense_context = SuspenseContext::downcast_suspense_boundary_from_scope( + &dom.runtime, + scope_id, + ) + .unwrap(); + suspense_context.set_suspended_nodes(new_suspended_nodes); - suspense_context.in_suspense_placeholder(&dom.runtime(), || { - old_children.move_node_to_background( - std::slice::from_ref(&new_placeholder), - parent, - dom, - to, - ); - }); + driver.store_children(&children); + } + // We have no suspended nodes, and we are not suspended. Just diff the children like normal + (None, false) => { + suspense_context.under_suspense_boundary(&dom.runtime(), || { + last_rendered_node.diff_node(&children, dom, to.as_mut()); + }); + + // Set the last rendered node to the new children + driver.store_children(&children); + dom.scopes[scope_id.0].last_rendered_node = Some(children); + } + // We have no suspended nodes, but we just became suspended. Move the children to the background + (None, true) => { + let old_children = last_rendered_node; + let new_children: VNode = children.as_vnode().clone(); + + let new_placeholder = + LastRenderedNode::new(fallback.call(suspense_context.clone())); + + // Move the children to the background + let mount = old_children.mount.get(); + let parent = dom.get_mounted_parent(mount); + + suspense_context.in_suspense_placeholder(&dom.runtime(), || { + old_children.move_node_to_background( + std::slice::from_ref(&new_placeholder), + parent, + dom, + to.as_mut(), + ); + }); + + // Then diff the new children in the background + suspense_context.under_suspense_boundary(&dom.runtime(), || { + old_children.diff_node( + &new_children, + dom, + Option::<&mut NoOpMutations>::None, + ); + }); - // Then diff the new children in the background - suspense_context.under_suspense_boundary(&dom.runtime(), || { - old_children.diff_node(&new_children, dom, None::<&mut M>); - }); + // Set the last rendered node to the new suspense placeholder + dom.scopes[scope_id.0].last_rendered_node = Some(new_placeholder); - // Set the last rendered node to the new suspense placeholder - dom.scopes[scope_id.0].last_rendered_node = Some(new_placeholder); + let suspense_context = SuspenseContext::downcast_suspense_boundary_from_scope( + &dom.runtime, + scope_id, + ) + .unwrap(); + suspense_context.set_suspended_nodes(new_children); - let suspense_context = SuspenseContext::downcast_suspense_boundary_from_scope( - &dom.runtime, - scope_id, - ) - .unwrap(); - suspense_context.set_suspended_nodes(new_children); - - un_resolve_suspense(dom, scope_id); - } - // We have suspended nodes, but we just got out of suspense. Move the suspended nodes to the foreground - (Some(_), false) => { - // Take the suspended nodes out of the suspense boundary so the children know that the boundary is not suspended while diffing - let old_suspended_nodes = suspense_context.take_suspended_nodes().unwrap(); - let old_placeholder = last_rendered_node; - let new_children = children; - - // First diff the two children nodes in the background - suspense_context.under_suspense_boundary(&dom.runtime(), || { - old_suspended_nodes.diff_node(&new_children, dom, None::<&mut M>); - - // Then replace the placeholder with the new children - let mount = old_placeholder.mount.get(); - let parent = dom.get_mounted_parent(mount); - old_placeholder.replace( - std::slice::from_ref(&new_children), - parent, - dom, - to, - ); - }); + driver.store_children(&children); + un_resolve_suspense(dom, scope_id); + } + // We have suspended nodes, but we just got out of suspense. Move the suspended nodes to the foreground + (Some(_), false) => { + // Take the suspended nodes out of the suspense boundary so the children know that the boundary is not suspended while diffing + let old_suspended_nodes = suspense_context.take_suspended_nodes().unwrap(); + let old_placeholder = last_rendered_node; + + // First diff the two children nodes in the background + suspense_context.under_suspense_boundary(&dom.runtime(), || { + old_suspended_nodes.diff_node( + &children, + dom, + Option::<&mut NoOpMutations>::None, + ); - // Set the last rendered node to the new children - dom.scopes[scope_id.0].last_rendered_node = Some(new_children); + // Then replace the placeholder with the new children + let mount = old_placeholder.mount.get(); + let parent = dom.get_mounted_parent(mount); + old_placeholder.replace( + std::slice::from_ref(&children), + parent, + dom, + to.as_mut(), + ); + }); - mark_suspense_resolved(&suspense_context, dom, scope_id); - } + // Set the last rendered node to the new children + driver.store_children(&children); + dom.scopes[scope_id.0].last_rendered_node = Some(children); + + mark_suspense_resolved(&suspense_context, dom, scope_id); } - }) - } + } + }) } /// Move to a resolved suspense state @@ -606,12 +650,10 @@ impl SuspenseContext { runtime: &Runtime, scope_id: ScopeId, ) -> Option { - runtime - .try_get_state(scope_id) - .and_then(|scope| scope.suspense_boundary()) + runtime.try_get_state(scope_id)?.suspense_boundary() } - pub(crate) fn remove_suspended_nodes( + pub(crate) fn remove_suspended_nodes( dom: &mut VirtualDom, scope_id: ScopeId, destroy_component_state: bool, @@ -622,7 +664,12 @@ impl SuspenseContext { }; // Remove the suspended nodes if let Some(node) = scope.take_suspended_nodes() { - node.remove_node_inner(dom, None::<&mut M>, destroy_component_state, None) + node.remove_node_inner( + dom, + Option::<&mut NoOpMutations>::None, + destroy_component_state, + None, + ) } } } diff --git a/packages/core/src/virtual_dom.rs b/packages/core/src/virtual_dom.rs index 5d31e69f6f..d1cdde2826 100644 --- a/packages/core/src/virtual_dom.rs +++ b/packages/core/src/virtual_dom.rs @@ -3,16 +3,17 @@ //! This module provides the primary mechanics to create a hook-based, concurrent VDOM for Rust. use crate::properties::RootProps; +use crate::render_driver::{BodyDriver, DynWriter}; use crate::root_wrapper::RootScopeWrapper; use crate::{ ComponentFunction, Element, Mutations, arena::ElementId, - innerlude::{NoOpMutations, SchedulerMsg, ScopeOrder, ScopeState, VProps, WriteMutations}, + innerlude::{NoOpMutations, SchedulerMsg, ScopeOrder, ScopeState, WriteMutations}, runtime::{Runtime, RuntimeGuard}, scopes::ScopeId, }; use crate::{Task, VComponent}; -use crate::{innerlude::Work, scopes::LastRenderedNode}; +use crate::innerlude::Work; use futures_util::StreamExt; use slab::Slab; use std::collections::BTreeSet; @@ -287,13 +288,8 @@ impl VirtualDom { root: impl ComponentFunction, root_props: P, ) -> Self { - let render_fn = root.fn_ptr(); - let props = VProps::new(root, |_, _| true, root_props, "Root"); - Self::new_with_component(VComponent { - name: "root", - render_fn, - props: Box::new(props), - }) + let driver = Rc::new(BodyDriver::new(root, |_, _| true, root_props, "Root")); + Self::new_with_component(VComponent::new_with_driver("root", driver)) } /// Create a new virtualdom and build it immediately @@ -316,13 +312,13 @@ impl VirtualDom { resolved_scopes: Default::default(), }; - let root = VProps::new( + let root_driver = Rc::new(BodyDriver::new( RootScopeWrapper, |_, _| true, RootProps(root), "RootWrapper", - ); - dom.new_scope(Box::new(root), "app"); + )); + dom.new_scope("app", root_driver); #[cfg(debug_assertions)] dom.register_subsecond_handler(); @@ -578,17 +574,14 @@ impl VirtualDom { #[instrument(skip(self, to), level = "trace", name = "VirtualDom::rebuild")] pub fn rebuild(&mut self, to: &mut impl WriteMutations) { let _runtime = RuntimeGuard::new(self.runtime.clone()); - let new_nodes = self + + let driver = self.runtime.get_state(ScopeId::ROOT).render_driver(); + let m = self .runtime .clone() - .while_rendering(|| self.run_scope(ScopeId::ROOT)); - - let new_nodes = LastRenderedNode::new(new_nodes); - - self.scopes[ScopeId::ROOT.0].last_rendered_node = Some(new_nodes.clone()); - - // Rebuilding implies we append the created elements to the root - let m = self.create_scope(Some(to), ScopeId::ROOT, new_nodes, None); + .while_rendering(|| { + driver.create(self, ScopeId::ROOT, true, None, DynWriter::erase(Some(to))) + }); to.append_children(ElementId(0), m); } From edc054fd062625f7f4fe0eef3d168d3172f09e0e Mon Sep 17 00:00:00 2001 From: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com> Date: Wed, 17 Jun 2026 00:23:36 +0000 Subject: [PATCH 02/16] fix(core): fix suspense resolution with RenderDriver trait - Add SuspenseBoundary variant to SuspenseLocation enum so boundary scopes properly report should_run_during_suspense() = true - Set SuspenseBoundary location on the boundary scope during creation - Move dirty_scopes.remove() in BodyDriver::create inside the new-scope-only branch to prevent premature removal of child scopes during suspense resolution's replace path - Make suspense_location field a RefCell to allow post-creation updates - Fix formatting issues Co-Authored-By: Evan Almloff --- packages/core/src/diff/component.rs | 14 ++-- packages/core/src/lib.rs | 2 +- packages/core/src/nodes.rs | 5 +- packages/core/src/render_driver.rs | 100 ++++++++++++++++-------- packages/core/src/scope_context.rs | 16 +++- packages/core/src/scopes.rs | 3 +- packages/core/src/suspense/component.rs | 45 +++++------ packages/core/src/virtual_dom.rs | 11 +-- 8 files changed, 115 insertions(+), 81 deletions(-) diff --git a/packages/core/src/diff/component.rs b/packages/core/src/diff/component.rs index 054c17468a..7420e2c647 100644 --- a/packages/core/src/diff/component.rs +++ b/packages/core/src/diff/component.rs @@ -1,11 +1,9 @@ use crate::{ - innerlude::{ - ElementRef, MountId, ScopeOrder, VComponent, WriteMutations, - }, + innerlude::{ElementRef, MountId, ScopeOrder, VComponent, WriteMutations}, + nodes::VNode, render_driver::DynWriter, scopes::{LastRenderedNode, ScopeId}, virtual_dom::VirtualDom, - nodes::VNode, }; impl VirtualDom { @@ -87,7 +85,13 @@ impl VirtualDom { replace_with: Option, ) { let driver = self.runtime.get_state(scope_id).render_driver(); - driver.remove(self, scope_id, DynWriter::erase(to), destroy_component_state, replace_with); + driver.remove( + self, + scope_id, + DynWriter::erase(to), + destroy_component_state, + replace_with, + ); } } diff --git a/packages/core/src/lib.rs b/packages/core/src/lib.rs index f236003d1d..5a341f0452 100644 --- a/packages/core/src/lib.rs +++ b/packages/core/src/lib.rs @@ -16,8 +16,8 @@ mod mutations; mod nodes; mod properties; mod reactive_context; -mod render_error; pub(crate) mod render_driver; +mod render_error; mod root_wrapper; mod runtime; mod scheduler; diff --git a/packages/core/src/nodes.rs b/packages/core/src/nodes.rs index 4052fc1119..e81c490de1 100644 --- a/packages/core/src/nodes.rs +++ b/packages/core/src/nodes.rs @@ -669,10 +669,7 @@ impl VComponent { } /// Create a [`VComponent`] with a custom [`RenderDriver`]. - pub(crate) fn new_with_driver( - name: &'static str, - driver: Rc, - ) -> Self { + pub(crate) fn new_with_driver(name: &'static str, driver: Rc) -> Self { VComponent { name, driver } } diff --git a/packages/core/src/render_driver.rs b/packages/core/src/render_driver.rs index 22960aad13..7972ca277b 100644 --- a/packages/core/src/render_driver.rs +++ b/packages/core/src/render_driver.rs @@ -27,21 +27,57 @@ impl<'a> DynWriter<'a> { } impl WriteMutations for DynWriter<'_> { - fn append_children(&mut self, id: ElementId, m: usize) { self.0.append_children(id, m) } - fn assign_node_id(&mut self, path: &'static [u8], id: ElementId) { self.0.assign_node_id(path, id) } - fn create_placeholder(&mut self, id: ElementId) { self.0.create_placeholder(id) } - fn create_text_node(&mut self, value: &str, id: ElementId) { self.0.create_text_node(value, id) } - fn load_template(&mut self, template: Template, index: usize, id: ElementId) { self.0.load_template(template, index, id) } - fn replace_node_with(&mut self, id: ElementId, m: usize) { self.0.replace_node_with(id, m) } - fn replace_placeholder_with_nodes(&mut self, path: &'static [u8], m: usize) { self.0.replace_placeholder_with_nodes(path, m) } - fn insert_nodes_after(&mut self, id: ElementId, m: usize) { self.0.insert_nodes_after(id, m) } - fn insert_nodes_before(&mut self, id: ElementId, m: usize) { self.0.insert_nodes_before(id, m) } - fn set_attribute(&mut self, name: &'static str, ns: Option<&'static str>, value: &AttributeValue, id: ElementId) { self.0.set_attribute(name, ns, value, id) } - fn set_node_text(&mut self, value: &str, id: ElementId) { self.0.set_node_text(value, id) } - fn create_event_listener(&mut self, name: &'static str, id: ElementId) { self.0.create_event_listener(name, id) } - fn remove_event_listener(&mut self, name: &'static str, id: ElementId) { self.0.remove_event_listener(name, id) } - fn remove_node(&mut self, id: ElementId) { self.0.remove_node(id) } - fn push_root(&mut self, id: ElementId) { self.0.push_root(id) } + fn append_children(&mut self, id: ElementId, m: usize) { + self.0.append_children(id, m) + } + fn assign_node_id(&mut self, path: &'static [u8], id: ElementId) { + self.0.assign_node_id(path, id) + } + fn create_placeholder(&mut self, id: ElementId) { + self.0.create_placeholder(id) + } + fn create_text_node(&mut self, value: &str, id: ElementId) { + self.0.create_text_node(value, id) + } + fn load_template(&mut self, template: Template, index: usize, id: ElementId) { + self.0.load_template(template, index, id) + } + fn replace_node_with(&mut self, id: ElementId, m: usize) { + self.0.replace_node_with(id, m) + } + fn replace_placeholder_with_nodes(&mut self, path: &'static [u8], m: usize) { + self.0.replace_placeholder_with_nodes(path, m) + } + fn insert_nodes_after(&mut self, id: ElementId, m: usize) { + self.0.insert_nodes_after(id, m) + } + fn insert_nodes_before(&mut self, id: ElementId, m: usize) { + self.0.insert_nodes_before(id, m) + } + fn set_attribute( + &mut self, + name: &'static str, + ns: Option<&'static str>, + value: &AttributeValue, + id: ElementId, + ) { + self.0.set_attribute(name, ns, value, id) + } + fn set_node_text(&mut self, value: &str, id: ElementId) { + self.0.set_node_text(value, id) + } + fn create_event_listener(&mut self, name: &'static str, id: ElementId) { + self.0.create_event_listener(name, id) + } + fn remove_event_listener(&mut self, name: &'static str, id: ElementId) { + self.0.remove_event_listener(name, id) + } + fn remove_node(&mut self, id: ElementId) { + self.0.remove_node(id) + } + fn push_root(&mut self, id: ElementId) { + self.0.push_root(id) + } } /// A scope's rendering lifecycle and the inputs it renders from. @@ -96,12 +132,7 @@ pub(crate) trait RenderDriver: 'static { ) -> usize; /// Diff this scope's output against its current props. - fn diff( - &self, - dom: &mut VirtualDom, - scope_id: ScopeId, - to: Option>, - ); + fn diff(&self, dom: &mut VirtualDom, scope_id: ScopeId, to: Option>); /// Remove this scope's output. When `destroy_component_state` is false /// the output is only being lifted out of the real DOM and the driver @@ -226,13 +257,13 @@ impl + Clone, P: Clone + 'static, M: 'static> RenderD if new { let body = dom.run_scope_with(scope_id, || self.render()); dom.scopes[scope_id.0].last_rendered_node = Some(LastRenderedNode::new(body)); - } - // If our scope landed in `dirty_scopes` during its initial render - // (e.g. a hook synchronously queued an update for itself), drain the - // entry now so we don't re-process the same scope after creation. - let height = dom.runtime.get_state(scope_id).height; - dom.dirty_scopes.remove(&ScopeOrder::new(height, scope_id)); + // If our scope landed in `dirty_scopes` during its initial render + // (e.g. a hook synchronously queued an update for itself), drain the + // entry now so we don't re-process the same scope after creation. + let height = dom.runtime.get_state(scope_id).height; + dom.dirty_scopes.remove(&ScopeOrder::new(height, scope_id)); + } let new_node = dom.scopes[scope_id.0] .last_rendered_node @@ -242,12 +273,7 @@ impl + Clone, P: Clone + 'static, M: 'static> RenderD dom.create_scope(to.as_mut(), scope_id, new_node, parent) } - fn diff( - &self, - dom: &mut VirtualDom, - scope_id: ScopeId, - mut to: Option>, - ) { + fn diff(&self, dom: &mut VirtualDom, scope_id: ScopeId, mut to: Option>) { let body = dom.run_scope_with(scope_id, || self.render()); dom.diff_scope(to.as_mut(), scope_id, body); } @@ -260,6 +286,12 @@ impl + Clone, P: Clone + 'static, M: 'static> RenderD destroy_component_state: bool, replace_with: Option, ) { - remove_rendered_output(dom, scope_id, to.as_mut(), destroy_component_state, replace_with); + remove_rendered_output( + dom, + scope_id, + to.as_mut(), + destroy_component_state, + replace_with, + ); } } diff --git a/packages/core/src/scope_context.rs b/packages/core/src/scope_context.rs index 1ce5494fdd..8abbc28fb9 100644 --- a/packages/core/src/scope_context.rs +++ b/packages/core/src/scope_context.rs @@ -25,6 +25,7 @@ pub(crate) enum ScopeStatus { pub(crate) enum SuspenseLocation { #[default] NotSuspended, + SuspenseBoundary(SuspenseContext), UnderSuspense(SuspenseContext), InSuspensePlaceholder(SuspenseContext), } @@ -34,6 +35,7 @@ impl SuspenseLocation { match self { SuspenseLocation::InSuspensePlaceholder(context) => Some(context), SuspenseLocation::UnderSuspense(context) => Some(context), + SuspenseLocation::SuspenseBoundary(context) => Some(context), _ => None, } } @@ -58,7 +60,7 @@ pub(crate) struct Scope { pub(crate) after_render: RefCell>>, /// The suspense boundary location this scope is rendered under, if any. - suspense_location: SuspenseLocation, + suspense_location: RefCell, /// The suspense context owned by this scope when this scope is a boundary. suspense_boundary: RefCell>, @@ -93,7 +95,7 @@ impl Scope { status: RefCell::new(ScopeStatus::Unmounted { effects_queued: Vec::new(), }), - suspense_location, + suspense_location: RefCell::new(suspense_location), suspense_boundary: RefCell::new(None), render_driver, } @@ -129,7 +131,12 @@ impl Scope { /// Get the suspense location of this scope pub(crate) fn suspense_location(&self) -> SuspenseLocation { - self.suspense_location.clone() + self.suspense_location.borrow().clone() + } + + /// Set the suspense location for this scope + pub(crate) fn set_suspense_location(&self, location: SuspenseLocation) { + *self.suspense_location.borrow_mut() = location; } /// If this scope is a suspense boundary, return the suspense context @@ -139,7 +146,8 @@ impl Scope { /// Check if a node should run during suspense pub(crate) fn should_run_during_suspense(&self) -> bool { - let Some(context) = self.suspense_location.suspense_context() else { + let location = self.suspense_location.borrow(); + let Some(context) = location.suspense_context() else { return false; }; diff --git a/packages/core/src/scopes.rs b/packages/core/src/scopes.rs index 798ce24003..ecf0ff1f6d 100644 --- a/packages/core/src/scopes.rs +++ b/packages/core/src/scopes.rs @@ -1,6 +1,5 @@ use crate::{ - Element, RenderError, Runtime, VNode, - reactive_context::ReactiveContext, scope_context::Scope, + Element, RenderError, Runtime, VNode, reactive_context::ReactiveContext, scope_context::Scope, }; use std::{cell::Ref, rc::Rc}; diff --git a/packages/core/src/suspense/component.rs b/packages/core/src/suspense/component.rs index df080c0e96..83090eaff5 100644 --- a/packages/core/src/suspense/component.rs +++ b/packages/core/src/suspense/component.rs @@ -310,17 +310,15 @@ impl RenderDriver for SuspenseDriver { let suspense_context = SuspenseContext::new(); let scope_state = dom.runtime.get_state(scope_id); scope_state.set_suspense_boundary(suspense_context.clone()); + scope_state.set_suspense_location( + crate::scope_context::SuspenseLocation::SuspenseBoundary(suspense_context.clone()), + ); suspense_context.mount(scope_id); } suspense_create(self, scope_id, parent, dom, to) } - fn diff( - &self, - dom: &mut VirtualDom, - scope_id: ScopeId, - to: Option>, - ) { + fn diff(&self, dom: &mut VirtualDom, scope_id: ScopeId, to: Option>) { suspense_diff(self, scope_id, dom, to) } @@ -337,7 +335,13 @@ impl RenderDriver for SuspenseDriver { // The scope's rendered output (children or fallback) is removed the // same way a plain component's output is. - remove_rendered_output(dom, scope_id, to.as_mut(), destroy_component_state, replace_with); + remove_rendered_output( + dom, + scope_id, + to.as_mut(), + destroy_component_state, + replace_with, + ); } } @@ -394,8 +398,9 @@ fn suspense_create( } else { // Otherwise just render the children in the real dom debug_assert!(children.mount.get().mounted()); - let nodes_created = suspense_context - .under_suspense_boundary(&dom.runtime(), || children.create(dom, parent, to.as_mut())); + let nodes_created = suspense_context.under_suspense_boundary(&dom.runtime(), || { + children.create(dom, parent, to.as_mut()) + }); dom.scopes[scope_id.0].last_rendered_node = children.into(); suspense_context.take_suspended_nodes(); mark_suspense_resolved(&suspense_context, dom, scope_id); @@ -521,11 +526,9 @@ fn suspense_diff( ); }); - let suspense_context = SuspenseContext::downcast_suspense_boundary_from_scope( - &dom.runtime, - scope_id, - ) - .unwrap(); + let suspense_context = + SuspenseContext::downcast_suspense_boundary_from_scope(&dom.runtime, scope_id) + .unwrap(); suspense_context.set_suspended_nodes(new_suspended_nodes); driver.store_children(&children); @@ -563,21 +566,15 @@ fn suspense_diff( // Then diff the new children in the background suspense_context.under_suspense_boundary(&dom.runtime(), || { - old_children.diff_node( - &new_children, - dom, - Option::<&mut NoOpMutations>::None, - ); + old_children.diff_node(&new_children, dom, Option::<&mut NoOpMutations>::None); }); // Set the last rendered node to the new suspense placeholder dom.scopes[scope_id.0].last_rendered_node = Some(new_placeholder); - let suspense_context = SuspenseContext::downcast_suspense_boundary_from_scope( - &dom.runtime, - scope_id, - ) - .unwrap(); + let suspense_context = + SuspenseContext::downcast_suspense_boundary_from_scope(&dom.runtime, scope_id) + .unwrap(); suspense_context.set_suspended_nodes(new_children); driver.store_children(&children); diff --git a/packages/core/src/virtual_dom.rs b/packages/core/src/virtual_dom.rs index d1cdde2826..b1da49c20a 100644 --- a/packages/core/src/virtual_dom.rs +++ b/packages/core/src/virtual_dom.rs @@ -2,6 +2,7 @@ //! //! This module provides the primary mechanics to create a hook-based, concurrent VDOM for Rust. +use crate::innerlude::Work; use crate::properties::RootProps; use crate::render_driver::{BodyDriver, DynWriter}; use crate::root_wrapper::RootScopeWrapper; @@ -13,7 +14,6 @@ use crate::{ scopes::ScopeId, }; use crate::{Task, VComponent}; -use crate::innerlude::Work; use futures_util::StreamExt; use slab::Slab; use std::collections::BTreeSet; @@ -576,12 +576,9 @@ impl VirtualDom { let _runtime = RuntimeGuard::new(self.runtime.clone()); let driver = self.runtime.get_state(ScopeId::ROOT).render_driver(); - let m = self - .runtime - .clone() - .while_rendering(|| { - driver.create(self, ScopeId::ROOT, true, None, DynWriter::erase(Some(to))) - }); + let m = self.runtime.clone().while_rendering(|| { + driver.create(self, ScopeId::ROOT, true, None, DynWriter::erase(Some(to))) + }); to.append_children(ElementId(0), m); } From f5e6893c56fa8ef56e88d3822b5e8b6682c2d3ea Mon Sep 17 00:00:00 2001 From: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com> Date: Wed, 17 Jun 2026 01:11:29 +0000 Subject: [PATCH 03/16] refactor: replace dyn RenderDriver + DynWriter with enum dispatch - Convert RenderDriver from trait object (dyn RenderDriver) to enum with Body(Rc) and Suspense(Rc) variants - Remove DynWriter entirely; methods are now generic over M: WriteMutations - Move suspense_boundary ownership from Scope into SuspenseDriver (SuspenseContext is now owned by the driver, not an optional field on every scope) - Scope::suspense_boundary() now delegates to RenderDriver::suspense_context() - BodyProps trait handles type erasure for generic component functions - All call sites use direct generic Option<&mut M> instead of DynWriter::erase() Co-Authored-By: Evan Almloff --- packages/core/src/diff/component.rs | 17 +- packages/core/src/nodes.rs | 8 +- packages/core/src/render_driver.rs | 346 +++++++++++------------- packages/core/src/scope_arena.rs | 4 +- packages/core/src/scope_context.rs | 17 +- packages/core/src/suspense/component.rs | 128 ++++----- packages/core/src/virtual_dom.rs | 20 +- 7 files changed, 249 insertions(+), 291 deletions(-) diff --git a/packages/core/src/diff/component.rs b/packages/core/src/diff/component.rs index 7420e2c647..8d1e661a7e 100644 --- a/packages/core/src/diff/component.rs +++ b/packages/core/src/diff/component.rs @@ -1,7 +1,6 @@ use crate::{ innerlude::{ElementRef, MountId, ScopeOrder, VComponent, WriteMutations}, nodes::VNode, - render_driver::DynWriter, scopes::{LastRenderedNode, ScopeId}, virtual_dom::VirtualDom, }; @@ -13,7 +12,7 @@ impl VirtualDom { scope_id: ScopeId, ) { let driver = self.runtime.get_state(scope_id).render_driver(); - driver.diff(self, scope_id, DynWriter::erase(to)); + driver.diff(self, scope_id, to); } #[tracing::instrument(skip(self, to), level = "trace", name = "VirtualDom::diff_scope")] @@ -85,13 +84,7 @@ impl VirtualDom { replace_with: Option, ) { let driver = self.runtime.get_state(scope_id).render_driver(); - driver.remove( - self, - scope_id, - DynWriter::erase(to), - destroy_component_state, - replace_with, - ); + driver.remove(self, scope_id, to, destroy_component_state, replace_with); } } @@ -109,7 +102,7 @@ impl VNode { ) { // Replace components whose drivers identify different components // (different driver type, or a different body function value) - if !old.driver.same_component(&*new.driver) { + if !old.driver.same_component(&new.driver) { return self.replace_vcomponent(mount, idx, new, parent, dom, to); } @@ -117,7 +110,7 @@ impl VNode { // The scope's driver still owns the live props, so there's no need to update anything // This also implicitly drops the new props since they're not used let scope_driver = dom.runtime.get_state(scope_id).render_driver(); - if scope_driver.memoize(new.driver.as_any()) { + if scope_driver.memoize(&new.driver) { return; } @@ -175,6 +168,6 @@ impl VNode { } let driver = dom.runtime.get_state(scope_id).render_driver(); - driver.create(dom, scope_id, new, parent, DynWriter::erase(to)) + driver.create(dom, scope_id, new, parent, to) } } diff --git a/packages/core/src/nodes.rs b/packages/core/src/nodes.rs index e81c490de1..75f3a22a15 100644 --- a/packages/core/src/nodes.rs +++ b/packages/core/src/nodes.rs @@ -633,7 +633,7 @@ pub struct VComponent { pub name: &'static str, /// The driver owning this component's rendering lifecycle and props. - pub(crate) driver: Rc, + pub(crate) driver: RenderDriver, } impl Clone for VComponent { @@ -655,12 +655,12 @@ impl VComponent { where P: Properties + 'static, { - let driver = Rc::new(BodyDriver::new( + let driver = RenderDriver::Body(Rc::new(BodyDriver::new( component,

::memoize, props, fn_name, - )); + ))); VComponent { name: fn_name, @@ -669,7 +669,7 @@ impl VComponent { } /// Create a [`VComponent`] with a custom [`RenderDriver`]. - pub(crate) fn new_with_driver(name: &'static str, driver: Rc) -> Self { + pub(crate) fn new_with_driver(name: &'static str, driver: RenderDriver) -> Self { VComponent { name, driver } } diff --git a/packages/core/src/render_driver.rs b/packages/core/src/render_driver.rs index 7972ca277b..d94571144f 100644 --- a/packages/core/src/render_driver.rs +++ b/packages/core/src/render_driver.rs @@ -1,155 +1,135 @@ use std::{any::Any, cell::RefCell, panic::AssertUnwindSafe, rc::Rc}; use crate::{ - AttributeValue, ComponentFunction, Element, Template, WriteMutations, - arena::ElementId, + ComponentFunction, Element, WriteMutations, innerlude::{CapturedPanic, ElementRef, ScopeOrder}, scopes::{LastRenderedNode, ScopeId}, + suspense::{SuspenseContext, SuspenseDriver}, virtual_dom::VirtualDom, }; -/// A sized wrapper around `&mut dyn WriteMutations` that itself implements -/// `WriteMutations`, letting the `dyn RenderDriver` layer bridge into the -/// generic (`M: WriteMutations + Sized`) diffing methods without requiring -/// `?Sized` bounds throughout the diff pipeline. -pub(crate) struct DynWriter<'a>(&'a mut dyn WriteMutations); +/// Type-erased interface for a plain component's props and render function. +/// +/// This handles the generic `` parameters of [`BodyDriver`] behind +/// a trait object so that [`RenderDriver::Body`] can store any component. +pub(crate) trait BodyProps: 'static { + fn as_any(&self) -> &dyn Any; -impl<'a> DynWriter<'a> { - /// Erase a generic `Option<&mut M>` into `Option<&mut DynWriter>`. - /// - /// The returned option borrows the original writer through the `DynWriter` - /// wrapper, so the caller must not use the original `to` while the wrapper - /// is alive. - #[inline] - pub fn erase(to: Option<&mut M>) -> Option> { - to.map(|m| DynWriter(m as &mut dyn WriteMutations)) - } -} + /// Whether `other` renders the same component as this driver. + fn same_component(&self, other: &dyn BodyProps) -> bool; -impl WriteMutations for DynWriter<'_> { - fn append_children(&mut self, id: ElementId, m: usize) { - self.0.append_children(id, m) - } - fn assign_node_id(&mut self, path: &'static [u8], id: ElementId) { - self.0.assign_node_id(path, id) - } - fn create_placeholder(&mut self, id: ElementId) { - self.0.create_placeholder(id) - } - fn create_text_node(&mut self, value: &str, id: ElementId) { - self.0.create_text_node(value, id) - } - fn load_template(&mut self, template: Template, index: usize, id: ElementId) { - self.0.load_template(template, index, id) - } - fn replace_node_with(&mut self, id: ElementId, m: usize) { - self.0.replace_node_with(id, m) - } - fn replace_placeholder_with_nodes(&mut self, path: &'static [u8], m: usize) { - self.0.replace_placeholder_with_nodes(path, m) - } - fn insert_nodes_after(&mut self, id: ElementId, m: usize) { - self.0.insert_nodes_after(id, m) - } - fn insert_nodes_before(&mut self, id: ElementId, m: usize) { - self.0.insert_nodes_before(id, m) - } - fn set_attribute( - &mut self, - name: &'static str, - ns: Option<&'static str>, - value: &AttributeValue, - id: ElementId, - ) { - self.0.set_attribute(name, ns, value, id) - } - fn set_node_text(&mut self, value: &str, id: ElementId) { - self.0.set_node_text(value, id) - } - fn create_event_listener(&mut self, name: &'static str, id: ElementId) { - self.0.create_event_listener(name, id) - } - fn remove_event_listener(&mut self, name: &'static str, id: ElementId) { - self.0.remove_event_listener(name, id) - } - fn remove_node(&mut self, id: ElementId) { - self.0.remove_node(id) - } - fn push_root(&mut self, id: ElementId) { - self.0.push_root(id) - } + /// Make this driver's props equal to `new_driver`'s. Returns whether the + /// props were equal and the scope can be memoized. + fn memoize(&self, new_driver: &dyn Any) -> bool; + + /// A fresh instance with cloned props. + fn duplicate(&self) -> Rc; + + /// Run the component body and return the rendered element. + fn render(&self) -> Element; } -/// A scope's rendering lifecycle and the inputs it renders from. +/// The rendering lifecycle driver for a scope. /// -/// Every scope owns exactly one driver, attached when its [`VComponent`] is -/// constructed and fixed for the scope's lifetime: plain components use -/// [`BodyDriver`], which owns the component function and its props and -/// mounts/diffs the element the body returns, while suspense components attach -/// drivers in their `into_vcomponent` that own their props and manage the -/// scope's `last_rendered_node` directly, with no body to run. +/// Every scope owns exactly one driver: plain components use +/// [`RenderDriver::Body`], which owns the component function and its props; +/// suspense boundaries use [`RenderDriver::Suspense`], which owns the +/// [`SuspenseContext`] and manages children/fallback rendering. /// -/// A driver instance is per component instance: the scope adopts a -/// [`Self::duplicate`] of the vnode's driver at creation so the live scope -/// never aliases inputs with a vnode, and [`Self::memoize`] is how a parent -/// render hands the scope its new inputs. -pub(crate) trait RenderDriver: 'static { - /// The driver as `Any`, for [`Self::memoize`] hand-offs between two - /// instances of the same driver type. - fn as_any(&self) -> &dyn Any; +/// Because this is an enum (not a trait object), its methods can be generic +/// over `M: WriteMutations`, using the same pattern as the rest of the codebase. +#[derive(Clone)] +pub(crate) enum RenderDriver { + Body(Rc), + Suspense(Rc), +} - /// Whether `other` renders the same component as this driver, i.e. a - /// scope rendered by this driver can be diffed in place against - /// `other`'s props rather than replaced. Two drivers of one type - /// identify the same component by default; [`BodyDriver`] also compares - /// its function value, since dynamic components can put different - /// functions of one type in a slot. - fn same_component(&self, other: &dyn RenderDriver) -> bool { - self.as_any().type_id() == other.as_any().type_id() +impl RenderDriver { + /// Whether `other` renders the same component as this driver. + pub fn same_component(&self, other: &RenderDriver) -> bool { + match (self, other) { + (RenderDriver::Body(a), RenderDriver::Body(b)) => a.same_component(&**b), + (RenderDriver::Suspense(_), RenderDriver::Suspense(_)) => true, + _ => false, + } } - /// Make this driver's props equal to `new_driver`'s (a driver of the - /// same concrete type, guaranteed by the [`Self::same_component`] - /// check). Returns whether the props were equal and the scope can be - /// memoized. - fn memoize(&self, new_driver: &dyn Any) -> bool; + /// Update this driver's props to match `other`'s. Returns `true` if the + /// props were equal (memoized). + pub fn memoize(&self, other: &RenderDriver) -> bool { + match (self, other) { + (RenderDriver::Body(a), RenderDriver::Body(b)) => a.memoize(b.as_any()), + (RenderDriver::Suspense(a), RenderDriver::Suspense(b)) => a.memoize(b), + _ => false, + } + } - /// A fresh driver instance with cloned props, for [`VComponent`] clones - /// and scope adoption. - /// - /// [`VComponent`]: crate::nodes::VComponent - fn duplicate(&self) -> Rc; + /// A fresh driver instance with cloned props. + pub fn duplicate(&self) -> RenderDriver { + match self { + RenderDriver::Body(b) => RenderDriver::Body(b.duplicate()), + RenderDriver::Suspense(s) => RenderDriver::Suspense(Rc::new(s.duplicate())), + } + } - /// Mount this scope's output. `new` is true when the scope was allocated - /// for this create and has never run or rendered. - fn create( + /// Mount this scope's output. + pub fn create( &self, dom: &mut VirtualDom, scope_id: ScopeId, new: bool, parent: Option, - to: Option>, - ) -> usize; + to: Option<&mut M>, + ) -> usize { + match self { + RenderDriver::Body(b) => body_create(b, dom, scope_id, new, parent, to), + RenderDriver::Suspense(s) => s.create(dom, scope_id, new, parent, to), + } + } /// Diff this scope's output against its current props. - fn diff(&self, dom: &mut VirtualDom, scope_id: ScopeId, to: Option>); + pub fn diff( + &self, + dom: &mut VirtualDom, + scope_id: ScopeId, + to: Option<&mut M>, + ) { + match self { + RenderDriver::Body(b) => body_diff(b, dom, scope_id, to), + RenderDriver::Suspense(s) => s.diff(dom, scope_id, to), + } + } - /// Remove this scope's output. When `destroy_component_state` is false - /// the output is only being lifted out of the real DOM and the driver - /// must keep component state alive. - fn remove( + /// Remove this scope's output. + pub fn remove( &self, dom: &mut VirtualDom, scope_id: ScopeId, - to: Option>, + to: Option<&mut M>, destroy_component_state: bool, replace_with: Option, - ); + ) { + match self { + RenderDriver::Body(_) => { + remove_rendered_output(dom, scope_id, to, destroy_component_state, replace_with) + } + RenderDriver::Suspense(s) => { + s.remove(dom, scope_id, to, destroy_component_state, replace_with) + } + } + } + + /// If this driver is a suspense boundary, return its context. + pub fn suspense_context(&self) -> Option { + match self { + RenderDriver::Suspense(s) => Some(s.context()), + _ => None, + } + } } /// Remove a scope's rendered output from the DOM, and drop the scope when -/// `destroy_component_state` is set. Shared by [`BodyDriver`] and drivers -/// whose output is removed the same way (suspense). +/// `destroy_component_state` is set. Shared by body and suspense drivers. pub(crate) fn remove_rendered_output( dom: &mut VirtualDom, scope_id: ScopeId, @@ -166,9 +146,46 @@ pub(crate) fn remove_rendered_output( } } -/// The rendering lifecycle of a plain component: the driver owns the -/// component function and its props, runs the body, and the element it -/// returns is the scope's rendered output. +/// Mount a plain component scope's output. +fn body_create( + body: &Rc, + dom: &mut VirtualDom, + scope_id: ScopeId, + new: bool, + parent: Option, + to: Option<&mut M>, +) -> usize { + if new { + let body_element = dom.run_scope_with(scope_id, || body.render()); + dom.scopes[scope_id.0].last_rendered_node = Some(LastRenderedNode::new(body_element)); + + // If our scope landed in `dirty_scopes` during its initial render + // (e.g. a hook synchronously queued an update for itself), drain the + // entry now so we don't re-process the same scope after creation. + let height = dom.runtime.get_state(scope_id).height; + dom.dirty_scopes.remove(&ScopeOrder::new(height, scope_id)); + } + + let new_node = dom.scopes[scope_id.0] + .last_rendered_node + .clone() + .expect("Component to be mounted"); + + dom.create_scope(to, scope_id, new_node, parent) +} + +/// Diff a plain component scope against its current output. +fn body_diff( + body: &Rc, + dom: &mut VirtualDom, + scope_id: ScopeId, + to: Option<&mut M>, +) { + let element = dom.run_scope_with(scope_id, || body.render()); + dom.diff_scope(to, scope_id, element); +} + +/// The concrete implementation of [`BodyProps`] for a given component function. pub(crate) struct BodyDriver, P, M> { render_fn: F, memo: fn(&mut P, &P) -> bool, @@ -192,37 +209,16 @@ impl + Clone, P: Clone + 'static, M: 'static> BodyDri phantom: std::marker::PhantomData, } } - - fn render(&self) -> Element { - fn render_inner(_name: &str, res: Result>) -> Element { - match res { - Ok(node) => node, - Err(err) => { - #[cfg(not(target_arch = "wasm32"))] - { - tracing::error!("Panic while rendering component `{_name}`: {err:?}"); - } - Element::Err(CapturedPanic(err).into()) - } - } - } - - let props = self.props.borrow().clone(); - render_inner( - self.name, - std::panic::catch_unwind(AssertUnwindSafe(move || self.render_fn.rebuild(props))), - ) - } } -impl + Clone, P: Clone + 'static, M: 'static> RenderDriver +impl + Clone, P: Clone + 'static, M: 'static> BodyProps for BodyDriver { fn as_any(&self) -> &dyn Any { self } - fn same_component(&self, other: &dyn RenderDriver) -> bool { + fn same_component(&self, other: &dyn BodyProps) -> bool { other .as_any() .downcast_ref::() @@ -236,7 +232,7 @@ impl + Clone, P: Clone + 'static, M: 'static> RenderD } } - fn duplicate(&self) -> Rc { + fn duplicate(&self) -> Rc { Rc::new(Self { render_fn: self.render_fn.clone(), memo: self.memo, @@ -246,52 +242,24 @@ impl + Clone, P: Clone + 'static, M: 'static> RenderD }) } - fn create( - &self, - dom: &mut VirtualDom, - scope_id: ScopeId, - new: bool, - parent: Option, - mut to: Option>, - ) -> usize { - if new { - let body = dom.run_scope_with(scope_id, || self.render()); - dom.scopes[scope_id.0].last_rendered_node = Some(LastRenderedNode::new(body)); - - // If our scope landed in `dirty_scopes` during its initial render - // (e.g. a hook synchronously queued an update for itself), drain the - // entry now so we don't re-process the same scope after creation. - let height = dom.runtime.get_state(scope_id).height; - dom.dirty_scopes.remove(&ScopeOrder::new(height, scope_id)); + fn render(&self) -> Element { + fn render_inner(_name: &str, res: Result>) -> Element { + match res { + Ok(node) => node, + Err(err) => { + #[cfg(not(target_arch = "wasm32"))] + { + tracing::error!("Panic while rendering component `{_name}`: {err:?}"); + } + Element::Err(CapturedPanic(err).into()) + } + } } - let new_node = dom.scopes[scope_id.0] - .last_rendered_node - .clone() - .expect("Component to be mounted"); - - dom.create_scope(to.as_mut(), scope_id, new_node, parent) - } - - fn diff(&self, dom: &mut VirtualDom, scope_id: ScopeId, mut to: Option>) { - let body = dom.run_scope_with(scope_id, || self.render()); - dom.diff_scope(to.as_mut(), scope_id, body); - } - - fn remove( - &self, - dom: &mut VirtualDom, - scope_id: ScopeId, - mut to: Option>, - destroy_component_state: bool, - replace_with: Option, - ) { - remove_rendered_output( - dom, - scope_id, - to.as_mut(), - destroy_component_state, - replace_with, - ); + let props = self.props.borrow().clone(); + render_inner( + self.name, + std::panic::catch_unwind(AssertUnwindSafe(move || self.render_fn.rebuild(props))), + ) } } diff --git a/packages/core/src/scope_arena.rs b/packages/core/src/scope_arena.rs index 3a48c543c4..64bd0082eb 100644 --- a/packages/core/src/scope_arena.rs +++ b/packages/core/src/scope_arena.rs @@ -1,5 +1,3 @@ -use std::rc::Rc; - use crate::{ Element, ReactiveContext, innerlude::{RenderError, ScopeOrder, ScopeState}, @@ -16,7 +14,7 @@ impl VirtualDom { pub(super) fn new_scope( &mut self, name: &'static str, - driver: Rc, + driver: RenderDriver, ) -> &mut ScopeState { let parent_id = self.runtime.try_current_scope_id(); let height = match parent_id.and_then(|id| self.runtime.try_get_state(id)) { diff --git a/packages/core/src/scope_context.rs b/packages/core/src/scope_context.rs index 8abbc28fb9..5e6e3c24a0 100644 --- a/packages/core/src/scope_context.rs +++ b/packages/core/src/scope_context.rs @@ -9,7 +9,6 @@ use std::{ any::Any, cell::{Cell, RefCell}, future::Future, - rc::Rc, sync::Arc, }; @@ -62,11 +61,8 @@ pub(crate) struct Scope { /// The suspense boundary location this scope is rendered under, if any. suspense_location: RefCell, - /// The suspense context owned by this scope when this scope is a boundary. - suspense_boundary: RefCell>, - /// The driver owning this scope's rendered output. - render_driver: Rc, + render_driver: RenderDriver, pub(crate) status: RefCell, } @@ -78,7 +74,7 @@ impl Scope { parent_id: Option, height: u32, suspense_location: SuspenseLocation, - render_driver: Rc, + render_driver: RenderDriver, ) -> Self { Self { name, @@ -96,7 +92,6 @@ impl Scope { effects_queued: Vec::new(), }), suspense_location: RefCell::new(suspense_location), - suspense_boundary: RefCell::new(None), render_driver, } } @@ -106,14 +101,10 @@ impl Scope { } /// The driver owning this scope's rendered output. - pub(crate) fn render_driver(&self) -> Rc { + pub(crate) fn render_driver(&self) -> RenderDriver { self.render_driver.clone() } - pub(crate) fn set_suspense_boundary(&self, context: SuspenseContext) { - self.suspense_boundary.replace(Some(context)); - } - fn sender(&self) -> futures_channel::mpsc::UnboundedSender { Runtime::current().sender.clone() } @@ -141,7 +132,7 @@ impl Scope { /// If this scope is a suspense boundary, return the suspense context pub(crate) fn suspense_boundary(&self) -> Option { - self.suspense_boundary.borrow().clone() + self.render_driver.suspense_context() } /// Check if a node should run during suspense diff --git a/packages/core/src/suspense/component.rs b/packages/core/src/suspense/component.rs index 83090eaff5..62c1acdf76 100644 --- a/packages/core/src/suspense/component.rs +++ b/packages/core/src/suspense/component.rs @@ -1,8 +1,8 @@ -use std::{any::Any, cell::RefCell, rc::Rc}; +use std::{cell::RefCell, rc::Rc}; use crate::{ innerlude::*, - render_driver::{DynWriter, RenderDriver, remove_rendered_output}, + render_driver::{RenderDriver, remove_rendered_output}, scope_context::SuspenseLocation, }; @@ -182,7 +182,10 @@ impl SuspenseBoundaryPropsWithOwner { _render_fn: impl ComponentFunction, ) -> VComponent { let component_name = std::any::type_name_of_val(&_render_fn); - VComponent::new_with_driver(component_name, Rc::new(SuspenseDriver::new(self))) + VComponent::new_with_driver( + component_name, + RenderDriver::Suspense(Rc::new(SuspenseDriver::new(self))), + ) } } impl Properties for SuspenseBoundaryPropsWithOwner { @@ -242,20 +245,31 @@ pub fn SuspenseBoundary(__props: SuspenseBoundaryProps) -> Element { unreachable!("SuspenseBoundary should not be called directly") } -/// The rendering lifecycle of a suspense boundary scope: children render in -/// the background first, and the scope's output is either the children or -/// the fallback depending on whether any descendant suspended. -struct SuspenseDriver { +/// The rendering lifecycle of a suspense boundary scope. +/// +/// The driver owns the [`SuspenseContext`] for this boundary and the +/// children/fallback props. Children render in the background first; the +/// scope's visible output is either the children or the fallback depending on +/// whether any descendant is suspended. +pub(crate) struct SuspenseDriver { + /// The suspense context owned by this boundary. + suspense_context: SuspenseContext, props: RefCell, } impl SuspenseDriver { fn new(props: SuspenseBoundaryPropsWithOwner) -> Self { Self { + suspense_context: SuspenseContext::new(), props: RefCell::new(props), } } + /// Get the suspense context for this boundary. + pub(crate) fn context(&self) -> SuspenseContext { + self.suspense_context.clone() + } + fn children(&self) -> LastRenderedNode { self.props.borrow().inner.children.clone() } @@ -267,66 +281,52 @@ impl SuspenseDriver { fn store_children(&self, children: &LastRenderedNode) { self.props.borrow_mut().inner.children.clone_from(children); } -} - -/// The suspense driver owning `scope_id`, which must be a suspense boundary -/// scope. -fn suspense_driver(dom: &VirtualDom, scope_id: ScopeId) -> Rc { - dom.runtime.get_state(scope_id).render_driver() -} - -fn as_suspense(driver: &Rc) -> &SuspenseDriver { - driver - .as_any() - .downcast_ref::() - .expect("suspense boundary scopes carry a SuspenseDriver") -} -impl RenderDriver for SuspenseDriver { - fn as_any(&self) -> &dyn Any { - self + /// Whether `other` has the same props (memoization check). + pub(crate) fn memoize(&self, other: &SuspenseDriver) -> bool { + Properties::memoize(&mut *self.props.borrow_mut(), &other.props.borrow()) } - fn memoize(&self, new_driver: &dyn Any) -> bool { - match new_driver.downcast_ref::() { - Some(new) => Properties::memoize(&mut *self.props.borrow_mut(), &new.props.borrow()), - None => false, - } + /// A fresh driver instance with cloned props and a new context. + pub(crate) fn duplicate(&self) -> SuspenseDriver { + Self::new(self.props.borrow().clone()) } - fn duplicate(&self) -> Rc { - Rc::new(Self::new(self.props.borrow().clone())) - } - - fn create( + /// Mount this suspense boundary scope's output. + pub(crate) fn create( &self, dom: &mut VirtualDom, scope_id: ScopeId, new: bool, parent: Option, - to: Option>, + to: Option<&mut M>, ) -> usize { if new { - let suspense_context = SuspenseContext::new(); + self.suspense_context.mount(scope_id); let scope_state = dom.runtime.get_state(scope_id); - scope_state.set_suspense_boundary(suspense_context.clone()); - scope_state.set_suspense_location( - crate::scope_context::SuspenseLocation::SuspenseBoundary(suspense_context.clone()), - ); - suspense_context.mount(scope_id); + scope_state.set_suspense_location(SuspenseLocation::SuspenseBoundary( + self.suspense_context.clone(), + )); } suspense_create(self, scope_id, parent, dom, to) } - fn diff(&self, dom: &mut VirtualDom, scope_id: ScopeId, to: Option>) { + /// Diff this suspense boundary scope against its current props. + pub(crate) fn diff( + &self, + dom: &mut VirtualDom, + scope_id: ScopeId, + to: Option<&mut M>, + ) { suspense_diff(self, scope_id, dom, to) } - fn remove( + /// Remove this suspense boundary scope's output. + pub(crate) fn remove( &self, dom: &mut VirtualDom, scope_id: ScopeId, - mut to: Option>, + to: Option<&mut M>, destroy_component_state: bool, replace_with: Option, ) { @@ -335,13 +335,7 @@ impl RenderDriver for SuspenseDriver { // The scope's rendered output (children or fallback) is removed the // same way a plain component's output is. - remove_rendered_output( - dom, - scope_id, - to.as_mut(), - destroy_component_state, - replace_with, - ); + remove_rendered_output(dom, scope_id, to, destroy_component_state, replace_with); } } @@ -362,15 +356,15 @@ use generational_box::Owner; /// Mount a suspense boundary scope: render the children in the background /// first, then mount either the children or the fallback depending on whether /// anything suspended. -fn suspense_create( +fn suspense_create( driver: &SuspenseDriver, scope_id: ScopeId, parent: Option, dom: &mut VirtualDom, - mut to: Option>, + mut to: Option<&mut M>, ) -> usize { dom.runtime.clone().with_scope_on_stack(scope_id, || { - let suspense_context = dom.runtime.get_state(scope_id).suspense_boundary().unwrap(); + let suspense_context = driver.context(); let children = driver.children(); @@ -389,7 +383,7 @@ fn suspense_create( suspense_context.set_suspended_nodes(children.as_vnode().clone()); let suspense_placeholder = LastRenderedNode::new(driver.fallback().call(suspense_context.clone())); - let nodes_created = suspense_placeholder.create(dom, parent, to.as_mut()); + let nodes_created = suspense_placeholder.create(dom, parent, to.as_deref_mut()); (suspense_placeholder, nodes_created) }); @@ -399,7 +393,7 @@ fn suspense_create( // Otherwise just render the children in the real dom debug_assert!(children.mount.get().mounted()); let nodes_created = suspense_context.under_suspense_boundary(&dom.runtime(), || { - children.create(dom, parent, to.as_mut()) + children.create(dom, parent, to.as_deref_mut()) }); dom.scopes[scope_id.0].last_rendered_node = children.into(); suspense_context.take_suspended_nodes(); @@ -444,7 +438,6 @@ impl SuspenseBoundaryProps { }; let driver = suspense_driver(dom, scope_id); - let driver = as_suspense(&driver); // Unmount any children to reset any scopes under this suspense boundary let children = driver.children(); @@ -482,11 +475,11 @@ impl SuspenseBoundaryProps { } /// Diff a suspense boundary scope against its current children/fallback props. -fn suspense_diff( +fn suspense_diff( driver: &SuspenseDriver, scope_id: ScopeId, dom: &mut VirtualDom, - mut to: Option>, + mut to: Option<&mut M>, ) { dom.runtime.clone().with_scope_on_stack(scope_id, || { let scope = &mut dom.scopes[scope_id.0]; @@ -510,7 +503,7 @@ fn suspense_diff( let new_placeholder = LastRenderedNode::new(fallback.call(suspense_context.clone())); - last_rendered_node.diff_node(&new_placeholder, dom, to.as_mut()); + last_rendered_node.diff_node(&new_placeholder, dom, to.as_deref_mut()); new_placeholder }); @@ -536,7 +529,7 @@ fn suspense_diff( // We have no suspended nodes, and we are not suspended. Just diff the children like normal (None, false) => { suspense_context.under_suspense_boundary(&dom.runtime(), || { - last_rendered_node.diff_node(&children, dom, to.as_mut()); + last_rendered_node.diff_node(&children, dom, to.as_deref_mut()); }); // Set the last rendered node to the new children @@ -560,7 +553,7 @@ fn suspense_diff( std::slice::from_ref(&new_placeholder), parent, dom, - to.as_mut(), + to.as_deref_mut(), ); }); @@ -601,7 +594,7 @@ fn suspense_diff( std::slice::from_ref(&children), parent, dom, - to.as_mut(), + to.as_deref_mut(), ); }); @@ -631,6 +624,15 @@ fn un_resolve_suspense(dom: &mut VirtualDom, scope_id: ScopeId) { dom.resolved_scopes.retain(|&id| id != scope_id); } +/// Get the SuspenseDriver for a given scope. +fn suspense_driver(dom: &VirtualDom, scope_id: ScopeId) -> Rc { + let driver = dom.runtime.get_state(scope_id).render_driver(); + match driver { + RenderDriver::Suspense(s) => s, + _ => panic!("expected suspense driver on scope"), + } +} + impl SuspenseContext { /// Run a closure under a suspense boundary pub(crate) fn under_suspense_boundary(&self, runtime: &Runtime, f: impl FnOnce() -> O) -> O { diff --git a/packages/core/src/virtual_dom.rs b/packages/core/src/virtual_dom.rs index b1da49c20a..be631fecc9 100644 --- a/packages/core/src/virtual_dom.rs +++ b/packages/core/src/virtual_dom.rs @@ -4,7 +4,7 @@ use crate::innerlude::Work; use crate::properties::RootProps; -use crate::render_driver::{BodyDriver, DynWriter}; +use crate::render_driver::{BodyDriver, RenderDriver}; use crate::root_wrapper::RootScopeWrapper; use crate::{ ComponentFunction, Element, Mutations, @@ -288,7 +288,12 @@ impl VirtualDom { root: impl ComponentFunction, root_props: P, ) -> Self { - let driver = Rc::new(BodyDriver::new(root, |_, _| true, root_props, "Root")); + let driver = RenderDriver::Body(Rc::new(BodyDriver::new( + root, + |_, _| true, + root_props, + "Root", + ))); Self::new_with_component(VComponent::new_with_driver("root", driver)) } @@ -312,12 +317,12 @@ impl VirtualDom { resolved_scopes: Default::default(), }; - let root_driver = Rc::new(BodyDriver::new( + let root_driver = RenderDriver::Body(Rc::new(BodyDriver::new( RootScopeWrapper, |_, _| true, RootProps(root), "RootWrapper", - )); + ))); dom.new_scope("app", root_driver); #[cfg(debug_assertions)] @@ -576,9 +581,10 @@ impl VirtualDom { let _runtime = RuntimeGuard::new(self.runtime.clone()); let driver = self.runtime.get_state(ScopeId::ROOT).render_driver(); - let m = self.runtime.clone().while_rendering(|| { - driver.create(self, ScopeId::ROOT, true, None, DynWriter::erase(Some(to))) - }); + let m = self + .runtime + .clone() + .while_rendering(|| driver.create(self, ScopeId::ROOT, true, None, Some(to))); to.append_children(ElementId(0), m); } From a5b7b0cf61aefd65fb598e9d5559eb1f6b13c630 Mon Sep 17 00:00:00 2001 From: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com> Date: Wed, 17 Jun 2026 01:51:15 +0000 Subject: [PATCH 04/16] refactor: use trait object RenderDriver with direct dyn WriteMutations, remove DynWriter - RenderDriver is now a trait (Rc) instead of an enum - Methods take Option<&mut dyn WriteMutations> directly, no DynWriter wrapper - Added blanket impl WriteMutations for &mut T where T: WriteMutations + ?Sized to allow passing &mut dyn WriteMutations through generic call chains - SuspenseDriver implements RenderDriver trait with suspense_context() method - suspense_boundary lives in the driver, not as an optional field on Scope - Call sites cast inline: to.map(|m| m as &mut dyn WriteMutations) Co-Authored-By: Evan Almloff --- packages/core/src/diff/component.rs | 22 ++- packages/core/src/mutations.rs | 54 ++++++ packages/core/src/nodes.rs | 8 +- packages/core/src/render_driver.rs | 247 +++++++++--------------- packages/core/src/scope_arena.rs | 4 +- packages/core/src/scope_context.rs | 7 +- packages/core/src/suspense/component.rs | 86 +++++---- packages/core/src/virtual_dom.rs | 25 +-- 8 files changed, 237 insertions(+), 216 deletions(-) diff --git a/packages/core/src/diff/component.rs b/packages/core/src/diff/component.rs index 8d1e661a7e..0b604510b9 100644 --- a/packages/core/src/diff/component.rs +++ b/packages/core/src/diff/component.rs @@ -12,7 +12,7 @@ impl VirtualDom { scope_id: ScopeId, ) { let driver = self.runtime.get_state(scope_id).render_driver(); - driver.diff(self, scope_id, to); + driver.diff(self, scope_id, to.map(|m| m as &mut dyn WriteMutations)); } #[tracing::instrument(skip(self, to), level = "trace", name = "VirtualDom::diff_scope")] @@ -84,7 +84,13 @@ impl VirtualDom { replace_with: Option, ) { let driver = self.runtime.get_state(scope_id).render_driver(); - driver.remove(self, scope_id, to, destroy_component_state, replace_with); + driver.remove( + self, + scope_id, + to.map(|m| m as &mut dyn WriteMutations), + destroy_component_state, + replace_with, + ); } } @@ -102,7 +108,7 @@ impl VNode { ) { // Replace components whose drivers identify different components // (different driver type, or a different body function value) - if !old.driver.same_component(&new.driver) { + if !old.driver.same_component(&*new.driver) { return self.replace_vcomponent(mount, idx, new, parent, dom, to); } @@ -110,7 +116,7 @@ impl VNode { // The scope's driver still owns the live props, so there's no need to update anything // This also implicitly drops the new props since they're not used let scope_driver = dom.runtime.get_state(scope_id).render_driver(); - if scope_driver.memoize(&new.driver) { + if scope_driver.memoize(new.driver.as_any()) { return; } @@ -168,6 +174,12 @@ impl VNode { } let driver = dom.runtime.get_state(scope_id).render_driver(); - driver.create(dom, scope_id, new, parent, to) + driver.create( + dom, + scope_id, + new, + parent, + to.map(|m| m as &mut dyn WriteMutations), + ) } } diff --git a/packages/core/src/mutations.rs b/packages/core/src/mutations.rs index eb1e485f19..9c47a3379d 100644 --- a/packages/core/src/mutations.rs +++ b/packages/core/src/mutations.rs @@ -117,6 +117,60 @@ pub trait WriteMutations { fn push_root(&mut self, id: ElementId); } +impl WriteMutations for &mut T { + fn append_children(&mut self, id: ElementId, m: usize) { + (**self).append_children(id, m) + } + fn assign_node_id(&mut self, path: &'static [u8], id: ElementId) { + (**self).assign_node_id(path, id) + } + fn create_placeholder(&mut self, id: ElementId) { + (**self).create_placeholder(id) + } + fn create_text_node(&mut self, value: &str, id: ElementId) { + (**self).create_text_node(value, id) + } + fn load_template(&mut self, template: Template, index: usize, id: ElementId) { + (**self).load_template(template, index, id) + } + fn replace_node_with(&mut self, id: ElementId, m: usize) { + (**self).replace_node_with(id, m) + } + fn replace_placeholder_with_nodes(&mut self, path: &'static [u8], m: usize) { + (**self).replace_placeholder_with_nodes(path, m) + } + fn insert_nodes_after(&mut self, id: ElementId, m: usize) { + (**self).insert_nodes_after(id, m) + } + fn insert_nodes_before(&mut self, id: ElementId, m: usize) { + (**self).insert_nodes_before(id, m) + } + fn set_attribute( + &mut self, + name: &'static str, + ns: Option<&'static str>, + value: &AttributeValue, + id: ElementId, + ) { + (**self).set_attribute(name, ns, value, id) + } + fn set_node_text(&mut self, value: &str, id: ElementId) { + (**self).set_node_text(value, id) + } + fn create_event_listener(&mut self, name: &'static str, id: ElementId) { + (**self).create_event_listener(name, id) + } + fn remove_event_listener(&mut self, name: &'static str, id: ElementId) { + (**self).remove_event_listener(name, id) + } + fn remove_node(&mut self, id: ElementId) { + (**self).remove_node(id) + } + fn push_root(&mut self, id: ElementId) { + (**self).push_root(id) + } +} + /// A `Mutation` represents a single instruction for the renderer to use to modify the UI tree to match the state /// of the Dioxus VirtualDom. /// diff --git a/packages/core/src/nodes.rs b/packages/core/src/nodes.rs index 75f3a22a15..5984e40f8d 100644 --- a/packages/core/src/nodes.rs +++ b/packages/core/src/nodes.rs @@ -633,7 +633,7 @@ pub struct VComponent { pub name: &'static str, /// The driver owning this component's rendering lifecycle and props. - pub(crate) driver: RenderDriver, + pub(crate) driver: Rc, } impl Clone for VComponent { @@ -655,12 +655,12 @@ impl VComponent { where P: Properties + 'static, { - let driver = RenderDriver::Body(Rc::new(BodyDriver::new( + let driver: Rc = Rc::new(BodyDriver::new( component,

::memoize, props, fn_name, - ))); + )); VComponent { name: fn_name, @@ -669,7 +669,7 @@ impl VComponent { } /// Create a [`VComponent`] with a custom [`RenderDriver`]. - pub(crate) fn new_with_driver(name: &'static str, driver: RenderDriver) -> Self { + pub(crate) fn new_with_driver(name: &'static str, driver: Rc) -> Self { VComponent { name, driver } } diff --git a/packages/core/src/render_driver.rs b/packages/core/src/render_driver.rs index d94571144f..c157587455 100644 --- a/packages/core/src/render_driver.rs +++ b/packages/core/src/render_driver.rs @@ -2,143 +2,73 @@ use std::{any::Any, cell::RefCell, panic::AssertUnwindSafe, rc::Rc}; use crate::{ ComponentFunction, Element, WriteMutations, - innerlude::{CapturedPanic, ElementRef, ScopeOrder}, + innerlude::{CapturedPanic, ElementRef, ScopeOrder, SuspenseContext}, scopes::{LastRenderedNode, ScopeId}, - suspense::{SuspenseContext, SuspenseDriver}, virtual_dom::VirtualDom, }; -/// Type-erased interface for a plain component's props and render function. +/// The rendering lifecycle driver for a scope. /// -/// This handles the generic `` parameters of [`BodyDriver`] behind -/// a trait object so that [`RenderDriver::Body`] can store any component. -pub(crate) trait BodyProps: 'static { +/// Every scope owns exactly one driver via `Rc`: +/// - Plain components use [`BodyDriver`], which owns the component function and props. +/// - Suspense boundaries use [`SuspenseDriver`](crate::suspense::SuspenseDriver), +/// which owns the [`SuspenseContext`] and manages children/fallback rendering. +pub(crate) trait RenderDriver: 'static { fn as_any(&self) -> &dyn Any; /// Whether `other` renders the same component as this driver. - fn same_component(&self, other: &dyn BodyProps) -> bool; + fn same_component(&self, other: &dyn RenderDriver) -> bool { + self.as_any().type_id() == other.as_any().type_id() + } - /// Make this driver's props equal to `new_driver`'s. Returns whether the - /// props were equal and the scope can be memoized. + /// Update this driver's props to match `new_driver`'s. Returns `true` if + /// the props were equal (memoized). fn memoize(&self, new_driver: &dyn Any) -> bool; /// A fresh instance with cloned props. - fn duplicate(&self) -> Rc; - - /// Run the component body and return the rendered element. - fn render(&self) -> Element; -} - -/// The rendering lifecycle driver for a scope. -/// -/// Every scope owns exactly one driver: plain components use -/// [`RenderDriver::Body`], which owns the component function and its props; -/// suspense boundaries use [`RenderDriver::Suspense`], which owns the -/// [`SuspenseContext`] and manages children/fallback rendering. -/// -/// Because this is an enum (not a trait object), its methods can be generic -/// over `M: WriteMutations`, using the same pattern as the rest of the codebase. -#[derive(Clone)] -pub(crate) enum RenderDriver { - Body(Rc), - Suspense(Rc), -} - -impl RenderDriver { - /// Whether `other` renders the same component as this driver. - pub fn same_component(&self, other: &RenderDriver) -> bool { - match (self, other) { - (RenderDriver::Body(a), RenderDriver::Body(b)) => a.same_component(&**b), - (RenderDriver::Suspense(_), RenderDriver::Suspense(_)) => true, - _ => false, - } - } + fn duplicate(&self) -> Rc; - /// Update this driver's props to match `other`'s. Returns `true` if the - /// props were equal (memoized). - pub fn memoize(&self, other: &RenderDriver) -> bool { - match (self, other) { - (RenderDriver::Body(a), RenderDriver::Body(b)) => a.memoize(b.as_any()), - (RenderDriver::Suspense(a), RenderDriver::Suspense(b)) => a.memoize(b), - _ => false, - } - } - - /// A fresh driver instance with cloned props. - pub fn duplicate(&self) -> RenderDriver { - match self { - RenderDriver::Body(b) => RenderDriver::Body(b.duplicate()), - RenderDriver::Suspense(s) => RenderDriver::Suspense(Rc::new(s.duplicate())), - } - } - - /// Mount this scope's output. - pub fn create( + /// Mount this scope's output. `to` receives DOM mutations; pass `None` for + /// background rendering (e.g. suspended children). + fn create( &self, dom: &mut VirtualDom, scope_id: ScopeId, new: bool, parent: Option, - to: Option<&mut M>, - ) -> usize { - match self { - RenderDriver::Body(b) => body_create(b, dom, scope_id, new, parent, to), - RenderDriver::Suspense(s) => s.create(dom, scope_id, new, parent, to), - } - } + to: Option<&mut dyn WriteMutations>, + ) -> usize; /// Diff this scope's output against its current props. - pub fn diff( - &self, - dom: &mut VirtualDom, - scope_id: ScopeId, - to: Option<&mut M>, - ) { - match self { - RenderDriver::Body(b) => body_diff(b, dom, scope_id, to), - RenderDriver::Suspense(s) => s.diff(dom, scope_id, to), - } - } + fn diff(&self, dom: &mut VirtualDom, scope_id: ScopeId, to: Option<&mut dyn WriteMutations>); /// Remove this scope's output. - pub fn remove( + fn remove( &self, dom: &mut VirtualDom, scope_id: ScopeId, - to: Option<&mut M>, + to: Option<&mut dyn WriteMutations>, destroy_component_state: bool, replace_with: Option, - ) { - match self { - RenderDriver::Body(_) => { - remove_rendered_output(dom, scope_id, to, destroy_component_state, replace_with) - } - RenderDriver::Suspense(s) => { - s.remove(dom, scope_id, to, destroy_component_state, replace_with) - } - } - } + ); /// If this driver is a suspense boundary, return its context. - pub fn suspense_context(&self) -> Option { - match self { - RenderDriver::Suspense(s) => Some(s.context()), - _ => None, - } + fn suspense_context(&self) -> Option { + None } } /// Remove a scope's rendered output from the DOM, and drop the scope when /// `destroy_component_state` is set. Shared by body and suspense drivers. -pub(crate) fn remove_rendered_output( +pub(crate) fn remove_rendered_output( dom: &mut VirtualDom, scope_id: ScopeId, - to: Option<&mut M>, + mut to: Option<&mut dyn WriteMutations>, destroy_component_state: bool, replace_with: Option, ) { if let Some(node) = dom.scopes[scope_id.0].last_rendered_node.clone() { - node.remove_node_inner(dom, to, destroy_component_state, replace_with) + node.remove_node_inner(dom, to.as_mut(), destroy_component_state, replace_with) }; if destroy_component_state { @@ -146,46 +76,7 @@ pub(crate) fn remove_rendered_output( } } -/// Mount a plain component scope's output. -fn body_create( - body: &Rc, - dom: &mut VirtualDom, - scope_id: ScopeId, - new: bool, - parent: Option, - to: Option<&mut M>, -) -> usize { - if new { - let body_element = dom.run_scope_with(scope_id, || body.render()); - dom.scopes[scope_id.0].last_rendered_node = Some(LastRenderedNode::new(body_element)); - - // If our scope landed in `dirty_scopes` during its initial render - // (e.g. a hook synchronously queued an update for itself), drain the - // entry now so we don't re-process the same scope after creation. - let height = dom.runtime.get_state(scope_id).height; - dom.dirty_scopes.remove(&ScopeOrder::new(height, scope_id)); - } - - let new_node = dom.scopes[scope_id.0] - .last_rendered_node - .clone() - .expect("Component to be mounted"); - - dom.create_scope(to, scope_id, new_node, parent) -} - -/// Diff a plain component scope against its current output. -fn body_diff( - body: &Rc, - dom: &mut VirtualDom, - scope_id: ScopeId, - to: Option<&mut M>, -) { - let element = dom.run_scope_with(scope_id, || body.render()); - dom.diff_scope(to, scope_id, element); -} - -/// The concrete implementation of [`BodyProps`] for a given component function. +/// The concrete driver for plain (non-suspense) components. pub(crate) struct BodyDriver, P, M> { render_fn: F, memo: fn(&mut P, &P) -> bool, @@ -209,16 +100,37 @@ impl + Clone, P: Clone + 'static, M: 'static> BodyDri phantom: std::marker::PhantomData, } } + + fn render(&self) -> Element { + fn render_inner(_name: &str, res: Result>) -> Element { + match res { + Ok(node) => node, + Err(err) => { + #[cfg(not(target_arch = "wasm32"))] + { + tracing::error!("Panic while rendering component `{_name}`: {err:?}"); + } + Element::Err(CapturedPanic(err).into()) + } + } + } + + let props = self.props.borrow().clone(); + render_inner( + self.name, + std::panic::catch_unwind(AssertUnwindSafe(move || self.render_fn.rebuild(props))), + ) + } } -impl + Clone, P: Clone + 'static, M: 'static> BodyProps +impl + Clone, P: Clone + 'static, M: 'static> RenderDriver for BodyDriver { fn as_any(&self) -> &dyn Any { self } - fn same_component(&self, other: &dyn BodyProps) -> bool { + fn same_component(&self, other: &dyn RenderDriver) -> bool { other .as_any() .downcast_ref::() @@ -232,7 +144,7 @@ impl + Clone, P: Clone + 'static, M: 'static> BodyPro } } - fn duplicate(&self) -> Rc { + fn duplicate(&self) -> Rc { Rc::new(Self { render_fn: self.render_fn.clone(), memo: self.memo, @@ -242,24 +154,45 @@ impl + Clone, P: Clone + 'static, M: 'static> BodyPro }) } - fn render(&self) -> Element { - fn render_inner(_name: &str, res: Result>) -> Element { - match res { - Ok(node) => node, - Err(err) => { - #[cfg(not(target_arch = "wasm32"))] - { - tracing::error!("Panic while rendering component `{_name}`: {err:?}"); - } - Element::Err(CapturedPanic(err).into()) - } - } + fn create( + &self, + dom: &mut VirtualDom, + scope_id: ScopeId, + new: bool, + parent: Option, + mut to: Option<&mut dyn WriteMutations>, + ) -> usize { + if new { + let body = dom.run_scope_with(scope_id, || self.render()); + dom.scopes[scope_id.0].last_rendered_node = Some(LastRenderedNode::new(body)); + let height = dom.runtime.get_state(scope_id).height; + dom.dirty_scopes.remove(&ScopeOrder::new(height, scope_id)); } + let new_node = dom.scopes[scope_id.0] + .last_rendered_node + .clone() + .expect("Component to be mounted"); + dom.create_scope(to.as_mut(), scope_id, new_node, parent) + } - let props = self.props.borrow().clone(); - render_inner( - self.name, - std::panic::catch_unwind(AssertUnwindSafe(move || self.render_fn.rebuild(props))), - ) + fn diff( + &self, + dom: &mut VirtualDom, + scope_id: ScopeId, + mut to: Option<&mut dyn WriteMutations>, + ) { + let body = dom.run_scope_with(scope_id, || self.render()); + dom.diff_scope(to.as_mut(), scope_id, body); + } + + fn remove( + &self, + dom: &mut VirtualDom, + scope_id: ScopeId, + to: Option<&mut dyn WriteMutations>, + destroy_component_state: bool, + replace_with: Option, + ) { + remove_rendered_output(dom, scope_id, to, destroy_component_state, replace_with); } } diff --git a/packages/core/src/scope_arena.rs b/packages/core/src/scope_arena.rs index 64bd0082eb..3a48c543c4 100644 --- a/packages/core/src/scope_arena.rs +++ b/packages/core/src/scope_arena.rs @@ -1,3 +1,5 @@ +use std::rc::Rc; + use crate::{ Element, ReactiveContext, innerlude::{RenderError, ScopeOrder, ScopeState}, @@ -14,7 +16,7 @@ impl VirtualDom { pub(super) fn new_scope( &mut self, name: &'static str, - driver: RenderDriver, + driver: Rc, ) -> &mut ScopeState { let parent_id = self.runtime.try_current_scope_id(); let height = match parent_id.and_then(|id| self.runtime.try_get_state(id)) { diff --git a/packages/core/src/scope_context.rs b/packages/core/src/scope_context.rs index 5e6e3c24a0..1e5dae34fc 100644 --- a/packages/core/src/scope_context.rs +++ b/packages/core/src/scope_context.rs @@ -9,6 +9,7 @@ use std::{ any::Any, cell::{Cell, RefCell}, future::Future, + rc::Rc, sync::Arc, }; @@ -62,7 +63,7 @@ pub(crate) struct Scope { suspense_location: RefCell, /// The driver owning this scope's rendered output. - render_driver: RenderDriver, + render_driver: Rc, pub(crate) status: RefCell, } @@ -74,7 +75,7 @@ impl Scope { parent_id: Option, height: u32, suspense_location: SuspenseLocation, - render_driver: RenderDriver, + render_driver: Rc, ) -> Self { Self { name, @@ -101,7 +102,7 @@ impl Scope { } /// The driver owning this scope's rendered output. - pub(crate) fn render_driver(&self) -> RenderDriver { + pub(crate) fn render_driver(&self) -> Rc { self.render_driver.clone() } diff --git a/packages/core/src/suspense/component.rs b/packages/core/src/suspense/component.rs index 62c1acdf76..4516dcbc22 100644 --- a/packages/core/src/suspense/component.rs +++ b/packages/core/src/suspense/component.rs @@ -1,4 +1,4 @@ -use std::{cell::RefCell, rc::Rc}; +use std::{any::Any, cell::RefCell, rc::Rc}; use crate::{ innerlude::*, @@ -184,7 +184,7 @@ impl SuspenseBoundaryPropsWithOwner { let component_name = std::any::type_name_of_val(&_render_fn); VComponent::new_with_driver( component_name, - RenderDriver::Suspense(Rc::new(SuspenseDriver::new(self))), + Rc::new(SuspenseDriver::new(self)) as Rc, ) } } @@ -281,25 +281,37 @@ impl SuspenseDriver { fn store_children(&self, children: &LastRenderedNode) { self.props.borrow_mut().inner.children.clone_from(children); } +} + +impl RenderDriver for SuspenseDriver { + fn as_any(&self) -> &dyn Any { + self + } - /// Whether `other` has the same props (memoization check). - pub(crate) fn memoize(&self, other: &SuspenseDriver) -> bool { - Properties::memoize(&mut *self.props.borrow_mut(), &other.props.borrow()) + fn same_component(&self, other: &dyn RenderDriver) -> bool { + other.as_any().downcast_ref::().is_some() + } + + fn memoize(&self, new_driver: &dyn Any) -> bool { + match new_driver.downcast_ref::() { + Some(other) => { + Properties::memoize(&mut *self.props.borrow_mut(), &other.props.borrow()) + } + None => false, + } } - /// A fresh driver instance with cloned props and a new context. - pub(crate) fn duplicate(&self) -> SuspenseDriver { - Self::new(self.props.borrow().clone()) + fn duplicate(&self) -> Rc { + Rc::new(Self::new(self.props.borrow().clone())) } - /// Mount this suspense boundary scope's output. - pub(crate) fn create( + fn create( &self, dom: &mut VirtualDom, scope_id: ScopeId, new: bool, parent: Option, - to: Option<&mut M>, + to: Option<&mut dyn WriteMutations>, ) -> usize { if new { self.suspense_context.mount(scope_id); @@ -311,22 +323,15 @@ impl SuspenseDriver { suspense_create(self, scope_id, parent, dom, to) } - /// Diff this suspense boundary scope against its current props. - pub(crate) fn diff( - &self, - dom: &mut VirtualDom, - scope_id: ScopeId, - to: Option<&mut M>, - ) { + fn diff(&self, dom: &mut VirtualDom, scope_id: ScopeId, to: Option<&mut dyn WriteMutations>) { suspense_diff(self, scope_id, dom, to) } - /// Remove this suspense boundary scope's output. - pub(crate) fn remove( + fn remove( &self, dom: &mut VirtualDom, scope_id: ScopeId, - to: Option<&mut M>, + to: Option<&mut dyn WriteMutations>, destroy_component_state: bool, replace_with: Option, ) { @@ -337,6 +342,10 @@ impl SuspenseDriver { // same way a plain component's output is. remove_rendered_output(dom, scope_id, to, destroy_component_state, replace_with); } + + fn suspense_context(&self) -> Option { + Some(self.suspense_context.clone()) + } } #[allow(non_snake_case)] @@ -356,12 +365,12 @@ use generational_box::Owner; /// Mount a suspense boundary scope: render the children in the background /// first, then mount either the children or the fallback depending on whether /// anything suspended. -fn suspense_create( +fn suspense_create( driver: &SuspenseDriver, scope_id: ScopeId, parent: Option, dom: &mut VirtualDom, - mut to: Option<&mut M>, + mut to: Option<&mut dyn WriteMutations>, ) -> usize { dom.runtime.clone().with_scope_on_stack(scope_id, || { let suspense_context = driver.context(); @@ -383,7 +392,7 @@ fn suspense_create( suspense_context.set_suspended_nodes(children.as_vnode().clone()); let suspense_placeholder = LastRenderedNode::new(driver.fallback().call(suspense_context.clone())); - let nodes_created = suspense_placeholder.create(dom, parent, to.as_deref_mut()); + let nodes_created = suspense_placeholder.create(dom, parent, to.as_mut()); (suspense_placeholder, nodes_created) }); @@ -393,7 +402,7 @@ fn suspense_create( // Otherwise just render the children in the real dom debug_assert!(children.mount.get().mounted()); let nodes_created = suspense_context.under_suspense_boundary(&dom.runtime(), || { - children.create(dom, parent, to.as_deref_mut()) + children.create(dom, parent, to.as_mut()) }); dom.scopes[scope_id.0].last_rendered_node = children.into(); suspense_context.take_suspended_nodes(); @@ -475,11 +484,11 @@ impl SuspenseBoundaryProps { } /// Diff a suspense boundary scope against its current children/fallback props. -fn suspense_diff( +fn suspense_diff( driver: &SuspenseDriver, scope_id: ScopeId, dom: &mut VirtualDom, - mut to: Option<&mut M>, + mut to: Option<&mut dyn WriteMutations>, ) { dom.runtime.clone().with_scope_on_stack(scope_id, || { let scope = &mut dom.scopes[scope_id.0]; @@ -503,7 +512,7 @@ fn suspense_diff( let new_placeholder = LastRenderedNode::new(fallback.call(suspense_context.clone())); - last_rendered_node.diff_node(&new_placeholder, dom, to.as_deref_mut()); + last_rendered_node.diff_node(&new_placeholder, dom, to.as_mut()); new_placeholder }); @@ -529,7 +538,7 @@ fn suspense_diff( // We have no suspended nodes, and we are not suspended. Just diff the children like normal (None, false) => { suspense_context.under_suspense_boundary(&dom.runtime(), || { - last_rendered_node.diff_node(&children, dom, to.as_deref_mut()); + last_rendered_node.diff_node(&children, dom, to.as_mut()); }); // Set the last rendered node to the new children @@ -553,7 +562,7 @@ fn suspense_diff( std::slice::from_ref(&new_placeholder), parent, dom, - to.as_deref_mut(), + to.as_mut(), ); }); @@ -594,7 +603,7 @@ fn suspense_diff( std::slice::from_ref(&children), parent, dom, - to.as_deref_mut(), + to.as_mut(), ); }); @@ -627,9 +636,18 @@ fn un_resolve_suspense(dom: &mut VirtualDom, scope_id: ScopeId) { /// Get the SuspenseDriver for a given scope. fn suspense_driver(dom: &VirtualDom, scope_id: ScopeId) -> Rc { let driver = dom.runtime.get_state(scope_id).render_driver(); - match driver { - RenderDriver::Suspense(s) => s, - _ => panic!("expected suspense driver on scope"), + // Safety: we know suspense boundary scopes have SuspenseDriver as their driver. + let ptr = Rc::into_raw(driver); + // Check the downcast via as_any + unsafe { + let dyn_ref = &*ptr; + assert!( + dyn_ref.as_any().is::(), + "expected suspense driver on scope" + ); + // Reconstruct Rc from the raw pointer + let typed_ptr = ptr as *const SuspenseDriver; + Rc::from_raw(typed_ptr) } } diff --git a/packages/core/src/virtual_dom.rs b/packages/core/src/virtual_dom.rs index be631fecc9..ee29115b73 100644 --- a/packages/core/src/virtual_dom.rs +++ b/packages/core/src/virtual_dom.rs @@ -288,12 +288,8 @@ impl VirtualDom { root: impl ComponentFunction, root_props: P, ) -> Self { - let driver = RenderDriver::Body(Rc::new(BodyDriver::new( - root, - |_, _| true, - root_props, - "Root", - ))); + let driver: Rc = + Rc::new(BodyDriver::new(root, |_, _| true, root_props, "Root")); Self::new_with_component(VComponent::new_with_driver("root", driver)) } @@ -317,12 +313,12 @@ impl VirtualDom { resolved_scopes: Default::default(), }; - let root_driver = RenderDriver::Body(Rc::new(BodyDriver::new( + let root_driver: Rc = Rc::new(BodyDriver::new( RootScopeWrapper, |_, _| true, RootProps(root), "RootWrapper", - ))); + )); dom.new_scope("app", root_driver); #[cfg(debug_assertions)] @@ -581,10 +577,15 @@ impl VirtualDom { let _runtime = RuntimeGuard::new(self.runtime.clone()); let driver = self.runtime.get_state(ScopeId::ROOT).render_driver(); - let m = self - .runtime - .clone() - .while_rendering(|| driver.create(self, ScopeId::ROOT, true, None, Some(to))); + let m = self.runtime.clone().while_rendering(|| { + driver.create( + self, + ScopeId::ROOT, + true, + None, + Some(to as &mut dyn WriteMutations), + ) + }); to.append_children(ElementId(0), m); } From 084dd45f41cc411f8931ff612ca04b6abf1f3805 Mon Sep 17 00:00:00 2001 From: Evan Almloff Date: Wed, 17 Jun 2026 08:38:57 -0500 Subject: [PATCH 05/16] remove fn suspense_context --- packages/core/src/render_driver.rs | 11 +++-------- packages/core/src/scope_context.rs | 5 ++++- packages/core/src/suspense/component.rs | 4 ---- 3 files changed, 7 insertions(+), 13 deletions(-) diff --git a/packages/core/src/render_driver.rs b/packages/core/src/render_driver.rs index c157587455..28ac565ddf 100644 --- a/packages/core/src/render_driver.rs +++ b/packages/core/src/render_driver.rs @@ -2,7 +2,7 @@ use std::{any::Any, cell::RefCell, panic::AssertUnwindSafe, rc::Rc}; use crate::{ ComponentFunction, Element, WriteMutations, - innerlude::{CapturedPanic, ElementRef, ScopeOrder, SuspenseContext}, + innerlude::{CapturedPanic, ElementRef, ScopeOrder}, scopes::{LastRenderedNode, ScopeId}, virtual_dom::VirtualDom, }; @@ -11,8 +11,8 @@ use crate::{ /// /// Every scope owns exactly one driver via `Rc`: /// - Plain components use [`BodyDriver`], which owns the component function and props. -/// - Suspense boundaries use [`SuspenseDriver`](crate::suspense::SuspenseDriver), -/// which owns the [`SuspenseContext`] and manages children/fallback rendering. +/// - Custom components may use specialized drivers, such as +/// [`SuspenseDriver`](crate::suspense::SuspenseDriver). pub(crate) trait RenderDriver: 'static { fn as_any(&self) -> &dyn Any; @@ -51,11 +51,6 @@ pub(crate) trait RenderDriver: 'static { destroy_component_state: bool, replace_with: Option, ); - - /// If this driver is a suspense boundary, return its context. - fn suspense_context(&self) -> Option { - None - } } /// Remove a scope's rendered output from the DOM, and drop the scope when diff --git a/packages/core/src/scope_context.rs b/packages/core/src/scope_context.rs index 1e5dae34fc..1acccdf87d 100644 --- a/packages/core/src/scope_context.rs +++ b/packages/core/src/scope_context.rs @@ -133,7 +133,10 @@ impl Scope { /// If this scope is a suspense boundary, return the suspense context pub(crate) fn suspense_boundary(&self) -> Option { - self.render_driver.suspense_context() + match &*self.suspense_location.borrow() { + SuspenseLocation::SuspenseBoundary(context) => Some(context.clone()), + _ => None, + } } /// Check if a node should run during suspense diff --git a/packages/core/src/suspense/component.rs b/packages/core/src/suspense/component.rs index 4516dcbc22..4f1eb281a0 100644 --- a/packages/core/src/suspense/component.rs +++ b/packages/core/src/suspense/component.rs @@ -342,10 +342,6 @@ impl RenderDriver for SuspenseDriver { // same way a plain component's output is. remove_rendered_output(dom, scope_id, to, destroy_component_state, replace_with); } - - fn suspense_context(&self) -> Option { - Some(self.suspense_context.clone()) - } } #[allow(non_snake_case)] From 3dd2e1366072b921cd4028b3f9d12cf46501767b Mon Sep 17 00:00:00 2001 From: Evan Almloff Date: Wed, 17 Jun 2026 08:55:59 -0500 Subject: [PATCH 06/16] use dyn everywhere --- packages/core/src/diff/component.rs | 40 ++++++---------- packages/core/src/diff/iterator.rs | 32 ++++++++----- packages/core/src/diff/mod.rs | 6 +-- packages/core/src/diff/node.rs | 63 +++++++++++++++---------- packages/core/src/render_driver.rs | 30 ++++++++---- packages/core/src/suspense/component.rs | 63 +++++++++++-------------- packages/core/src/virtual_dom.rs | 21 ++++----- 7 files changed, 130 insertions(+), 125 deletions(-) diff --git a/packages/core/src/diff/component.rs b/packages/core/src/diff/component.rs index 0b604510b9..95c6a70083 100644 --- a/packages/core/src/diff/component.rs +++ b/packages/core/src/diff/component.rs @@ -6,19 +6,19 @@ use crate::{ }; impl VirtualDom { - pub(crate) fn run_and_diff_scope( + pub(crate) fn run_and_diff_scope( &mut self, - to: Option<&mut M>, + to: Option<&mut (dyn WriteMutations + '_)>, scope_id: ScopeId, ) { let driver = self.runtime.get_state(scope_id).render_driver(); - driver.diff(self, scope_id, to.map(|m| m as &mut dyn WriteMutations)); + driver.diff(self, scope_id, to); } #[tracing::instrument(skip(self, to), level = "trace", name = "VirtualDom::diff_scope")] - pub(crate) fn diff_scope( + pub(crate) fn diff_scope( &mut self, - to: Option<&mut M>, + to: Option<&mut (dyn WriteMutations + '_)>, scope: ScopeId, new_nodes: crate::Element, ) { @@ -49,9 +49,9 @@ impl VirtualDom { /// /// Returns the number of nodes created on the stack #[tracing::instrument(skip(self, to), level = "trace", name = "VirtualDom::create_scope")] - pub(crate) fn create_scope( + pub(crate) fn create_scope( &mut self, - to: Option<&mut M>, + to: Option<&mut (dyn WriteMutations + '_)>, scope: ScopeId, new_nodes: LastRenderedNode, parent: Option, @@ -76,21 +76,15 @@ impl VirtualDom { }) } - pub(crate) fn remove_component_node( + pub(crate) fn remove_component_node( &mut self, - to: Option<&mut M>, + to: Option<&mut (dyn WriteMutations + '_)>, destroy_component_state: bool, scope_id: ScopeId, replace_with: Option, ) { let driver = self.runtime.get_state(scope_id).render_driver(); - driver.remove( - self, - scope_id, - to.map(|m| m as &mut dyn WriteMutations), - destroy_component_state, - replace_with, - ); + driver.remove(self, scope_id, to, destroy_component_state, replace_with); } } @@ -104,7 +98,7 @@ impl VNode { scope_id: ScopeId, parent: Option, dom: &mut VirtualDom, - to: Option<&mut impl WriteMutations>, + to: Option<&mut (dyn WriteMutations + '_)>, ) { // Replace components whose drivers identify different components // (different driver type, or a different body function value) @@ -134,7 +128,7 @@ impl VNode { new: &VComponent, parent: Option, dom: &mut VirtualDom, - mut to: Option<&mut impl WriteMutations>, + mut to: Option<&mut (dyn WriteMutations + '_)>, ) { let scope = ScopeId(dom.get_mounted_dyn_node(mount, idx)); @@ -156,7 +150,7 @@ impl VNode { component: &VComponent, parent: Option, dom: &mut VirtualDom, - to: Option<&mut impl WriteMutations>, + to: Option<&mut (dyn WriteMutations + '_)>, ) -> usize { let mut scope_id = ScopeId(dom.get_mounted_dyn_node(mount, idx)); let new = scope_id.is_placeholder(); @@ -174,12 +168,6 @@ impl VNode { } let driver = dom.runtime.get_state(scope_id).render_driver(); - driver.create( - dom, - scope_id, - new, - parent, - to.map(|m| m as &mut dyn WriteMutations), - ) + driver.create(dom, scope_id, new, parent, to) } } diff --git a/packages/core/src/diff/iterator.rs b/packages/core/src/diff/iterator.rs index 194ecec55e..634b6f128c 100644 --- a/packages/core/src/diff/iterator.rs +++ b/packages/core/src/diff/iterator.rs @@ -9,7 +9,7 @@ use rustc_hash::{FxHashMap, FxHashSet}; impl VirtualDom { pub(crate) fn diff_non_empty_fragment( &mut self, - to: Option<&mut impl WriteMutations>, + to: Option<&mut (dyn WriteMutations + '_)>, old: &[VNode], new: &[VNode], parent: Option, @@ -42,7 +42,7 @@ impl VirtualDom { // the change list stack is in the same state when this function returns. fn diff_non_keyed_children( &mut self, - mut to: Option<&mut impl WriteMutations>, + mut to: Option<&mut (dyn WriteMutations + '_)>, old: &[VNode], new: &[VNode], parent: Option, @@ -87,7 +87,7 @@ impl VirtualDom { // The stack is empty upon entry. fn diff_keyed_children( &mut self, - mut to: Option<&mut impl WriteMutations>, + mut to: Option<&mut (dyn WriteMutations + '_)>, old: &[VNode], new: &[VNode], parent: Option, @@ -156,7 +156,7 @@ impl VirtualDom { /// If there is no offset, then this function returns None and the diffing is complete. fn diff_keyed_ends( &mut self, - mut to: Option<&mut impl WriteMutations>, + mut to: Option<&mut (dyn WriteMutations + '_)>, old: &[VNode], new: &[VNode], parent: Option, @@ -238,7 +238,7 @@ impl VirtualDom { #[allow(clippy::too_many_lines)] fn diff_keyed_middle( &mut self, - mut to: Option<&mut impl WriteMutations>, + mut to: Option<&mut (dyn WriteMutations + '_)>, old: &[VNode], new: &[VNode], parent: Option, @@ -346,7 +346,7 @@ impl VirtualDom { vdom: &mut VirtualDom, new: &[VNode], old: &[VNode], - mut to: Option<&mut impl WriteMutations>, + mut to: Option<&mut (dyn WriteMutations + '_)>, parent: Option, new_index_to_old_index: &[usize], range: std::ops::Range, @@ -431,7 +431,7 @@ impl VirtualDom { fn create_and_insert_before( &mut self, - mut to: Option<&mut impl WriteMutations>, + mut to: Option<&mut (dyn WriteMutations + '_)>, new: &[VNode], before: &VNode, parent: Option, @@ -440,7 +440,12 @@ impl VirtualDom { self.insert_before(to, m, before); } - fn insert_before(&mut self, to: Option<&mut impl WriteMutations>, new: usize, before: &VNode) { + fn insert_before( + &mut self, + to: Option<&mut (dyn WriteMutations + '_)>, + new: usize, + before: &VNode, + ) { if let Some(to) = to { if new > 0 { let id = before.find_first_element(self); @@ -451,7 +456,7 @@ impl VirtualDom { fn create_and_insert_after( &mut self, - mut to: Option<&mut impl WriteMutations>, + mut to: Option<&mut (dyn WriteMutations + '_)>, new: &[VNode], after: &VNode, parent: Option, @@ -460,7 +465,12 @@ impl VirtualDom { self.insert_after(to, m, after); } - fn insert_after(&mut self, to: Option<&mut impl WriteMutations>, new: usize, after: &VNode) { + fn insert_after( + &mut self, + to: Option<&mut (dyn WriteMutations + '_)>, + new: usize, + after: &VNode, + ) { if let Some(to) = to { if new > 0 { let id = after.find_last_element(self); @@ -475,7 +485,7 @@ impl VNode { pub(crate) fn push_all_root_nodes( &self, dom: &VirtualDom, - to: &mut impl WriteMutations, + to: &mut (dyn WriteMutations + '_), ) -> usize { let template = self.template; diff --git a/packages/core/src/diff/mod.rs b/packages/core/src/diff/mod.rs index 7a7a89ee7b..3ea016b831 100644 --- a/packages/core/src/diff/mod.rs +++ b/packages/core/src/diff/mod.rs @@ -5,7 +5,7 @@ //! - Diffing nodes that are not mounted //! - Mounted nodes that have already been created //! -//! To support those cases, we lazily create components and only optionally write to the real dom while diffing with Option<&mut impl WriteMutations> +//! To support those cases, we lazily create components and only optionally write to the real dom while diffing with Option<&mut (dyn WriteMutations + '_)> #![allow(clippy::too_many_arguments)] @@ -24,7 +24,7 @@ mod node; impl VirtualDom { pub(crate) fn create_children( &mut self, - mut to: Option<&mut impl WriteMutations>, + mut to: Option<&mut (dyn WriteMutations + '_)>, nodes: &[VNode], parent: Option, ) -> usize { @@ -78,7 +78,7 @@ impl VirtualDom { /// Wont generate mutations for the inner nodes fn remove_nodes( &mut self, - mut to: Option<&mut impl WriteMutations>, + mut to: Option<&mut (dyn WriteMutations + '_)>, nodes: &[VNode], replace_with: Option, ) { diff --git a/packages/core/src/diff/node.rs b/packages/core/src/diff/node.rs index c39a2bebf8..0998f33f0a 100644 --- a/packages/core/src/diff/node.rs +++ b/packages/core/src/diff/node.rs @@ -16,7 +16,7 @@ impl VNode { &self, new: &VNode, dom: &mut VirtualDom, - mut to: Option<&mut impl WriteMutations>, + mut to: Option<&mut (dyn WriteMutations + '_)>, ) { // The node we are diffing from should always be mounted debug_assert!( @@ -82,7 +82,7 @@ impl VNode { old_node: &DynamicNode, new_node: &DynamicNode, dom: &mut VirtualDom, - mut to: Option<&mut impl WriteMutations>, + mut to: Option<&mut (dyn WriteMutations + '_)>, ) { tracing::trace!("diffing dynamic node from {old_node:?} to {new_node:?}"); match (old_node, new_node) { @@ -212,7 +212,13 @@ impl VNode { /// Diff the two text nodes /// /// This just sets the text of the node if it's different. - fn diff_vtext(&self, to: &mut impl WriteMutations, id: ElementId, left: &VText, right: &VText) { + fn diff_vtext( + &self, + to: &mut (dyn WriteMutations + '_), + id: ElementId, + left: &VText, + right: &VText, + ) { if left.value != right.value { to.set_node_text(&right.value, id); } @@ -223,7 +229,7 @@ impl VNode { right: &[VNode], parent: Option, dom: &mut VirtualDom, - to: Option<&mut impl WriteMutations>, + to: Option<&mut (dyn WriteMutations + '_)>, ) { self.replace_inner(right, parent, dom, to, true) } @@ -236,7 +242,7 @@ impl VNode { right: &[VNode], parent: Option, dom: &mut VirtualDom, - to: Option<&mut impl WriteMutations>, + to: Option<&mut (dyn WriteMutations + '_)>, ) { self.replace_inner(right, parent, dom, to, false) } @@ -246,7 +252,7 @@ impl VNode { right: &[VNode], parent: Option, dom: &mut VirtualDom, - mut to: Option<&mut impl WriteMutations>, + mut to: Option<&mut (dyn WriteMutations + '_)>, destroy_component_state: bool, ) { let m = dom.create_children(to.as_deref_mut(), right, parent); @@ -256,20 +262,20 @@ impl VNode { } /// Remove a node from the dom and potentially replace it with the top m nodes from the stack - pub(crate) fn remove_node( + pub(crate) fn remove_node( &self, dom: &mut VirtualDom, - to: Option<&mut M>, + to: Option<&mut (dyn WriteMutations + '_)>, replace_with: Option, ) { self.remove_node_inner(dom, to, true, replace_with) } /// Remove a node, but only maybe destroy the component state of that node. During suspense, we need to remove a node from the real dom without wiping the component state - pub(crate) fn remove_node_inner( + pub(crate) fn remove_node_inner( &self, dom: &mut VirtualDom, - to: Option<&mut M>, + to: Option<&mut (dyn WriteMutations + '_)>, destroy_component_state: bool, replace_with: Option, ) { @@ -285,7 +291,7 @@ impl VNode { // Remove the nested dynamic nodes // We don't generate mutations for these, as they will be removed by the parent (in the next line) // But we still need to make sure to reclaim them from the arena and drop their hooks, etc - self.remove_nested_dyn_nodes::(mount, dom, destroy_component_state); + self.remove_nested_dyn_nodes(mount, dom, destroy_component_state); // Clean up the roots, assuming we need to generate mutations for these // This is done last in order to preserve Node ID reclaim order (reclaim in reverse order of claim) @@ -302,7 +308,7 @@ impl VNode { &self, mount: MountId, dom: &mut VirtualDom, - mut to: Option<&mut impl WriteMutations>, + mut to: Option<&mut (dyn WriteMutations + '_)>, destroy_component_state: bool, replace_with: Option, ) { @@ -335,7 +341,7 @@ impl VNode { } } - fn remove_nested_dyn_nodes( + fn remove_nested_dyn_nodes( &self, mount: MountId, dom: &mut VirtualDom, @@ -349,7 +355,7 @@ impl VNode { self.remove_dynamic_node( mount, dom, - Option::<&mut M>::None, + None, destroy_component_state, idx, dyn_node, @@ -363,7 +369,7 @@ impl VNode { &self, mount: MountId, dom: &mut VirtualDom, - mut to: Option<&mut impl WriteMutations>, + mut to: Option<&mut (dyn WriteMutations + '_)>, destroy_component_state: bool, idx: usize, node: &DynamicNode, @@ -417,7 +423,7 @@ impl VNode { &self, new: &VNode, dom: &mut VirtualDom, - to: &mut impl WriteMutations, + to: &mut (dyn WriteMutations + '_), ) { let mount_id = new.mount.get(); for (idx, (old_attrs, new_attrs)) in self @@ -497,7 +503,12 @@ impl VNode { } } - fn remove_attribute(&self, attribute: &Attribute, id: ElementId, to: &mut impl WriteMutations) { + fn remove_attribute( + &self, + attribute: &Attribute, + id: ElementId, + to: &mut (dyn WriteMutations + '_), + ) { match &attribute.value { AttributeValue::Listener(_) => { to.remove_event_listener(&attribute.name[2..], id); @@ -520,7 +531,7 @@ impl VNode { id: ElementId, mount: MountId, dom: &mut VirtualDom, - to: &mut impl WriteMutations, + to: &mut (dyn WriteMutations + '_), ) { match &attribute.value { AttributeValue::Listener(_) => { @@ -543,7 +554,7 @@ impl VNode { &self, dom: &mut VirtualDom, parent: Option, - mut to: Option<&mut impl WriteMutations>, + mut to: Option<&mut (dyn WriteMutations + '_)>, ) -> usize { // Get the most up to date template let template = self.template; @@ -657,7 +668,7 @@ impl VNode { mount: MountId, dynamic_node_id: usize, dom: &mut VirtualDom, - to: Option<&mut impl WriteMutations>, + to: Option<&mut (dyn WriteMutations + '_)>, ) -> usize { use DynamicNode::*; match node { @@ -714,7 +725,7 @@ impl VNode { dynamic_nodes_iter: &mut Peekable>, root_idx: u8, dom: &mut VirtualDom, - mut to: Option<&mut impl WriteMutations>, + mut to: Option<&mut (dyn WriteMutations + '_)>, ) { fn collect_dyn_node_range( dynamic_nodes: &mut Peekable>, @@ -797,7 +808,7 @@ impl VNode { dynamic_attributes_iter: &mut Peekable>, root_idx: u8, dom: &mut VirtualDom, - to: &mut impl WriteMutations, + to: &mut (dyn WriteMutations + '_), ) { let mut last_path = None; // Only take nodes that are under this root node @@ -834,7 +845,7 @@ impl VNode { mount: MountId, root_idx: usize, dom: &mut VirtualDom, - to: &mut impl WriteMutations, + to: &mut (dyn WriteMutations + '_), ) -> ElementId { // Get an ID for this root since it's a real root let this_id = dom.next_element(); @@ -857,7 +868,7 @@ impl VNode { mount: MountId, path: &'static [u8], dom: &mut VirtualDom, - to: &mut impl WriteMutations, + to: &mut (dyn WriteMutations + '_), ) -> ElementId { // This is just the root node. We already know it's id if let [root_idx] = path { @@ -878,7 +889,7 @@ impl VNode { idx: usize, text: &VText, dom: &mut VirtualDom, - to: &mut impl WriteMutations, + to: &mut (dyn WriteMutations + '_), ) -> usize { let new_id = mount.mount_node(idx, dom); @@ -893,7 +904,7 @@ impl VNode { mount: MountId, idx: usize, dom: &mut VirtualDom, - to: &mut impl WriteMutations, + to: &mut (dyn WriteMutations + '_), ) -> usize { let new_id = mount.mount_node(idx, dom); diff --git a/packages/core/src/render_driver.rs b/packages/core/src/render_driver.rs index 28ac565ddf..c0abc23f7b 100644 --- a/packages/core/src/render_driver.rs +++ b/packages/core/src/render_driver.rs @@ -36,18 +36,23 @@ pub(crate) trait RenderDriver: 'static { scope_id: ScopeId, new: bool, parent: Option, - to: Option<&mut dyn WriteMutations>, + to: Option<&mut (dyn WriteMutations + '_)>, ) -> usize; /// Diff this scope's output against its current props. - fn diff(&self, dom: &mut VirtualDom, scope_id: ScopeId, to: Option<&mut dyn WriteMutations>); + fn diff( + &self, + dom: &mut VirtualDom, + scope_id: ScopeId, + to: Option<&mut (dyn WriteMutations + '_)>, + ); /// Remove this scope's output. fn remove( &self, dom: &mut VirtualDom, scope_id: ScopeId, - to: Option<&mut dyn WriteMutations>, + to: Option<&mut (dyn WriteMutations + '_)>, destroy_component_state: bool, replace_with: Option, ); @@ -58,12 +63,17 @@ pub(crate) trait RenderDriver: 'static { pub(crate) fn remove_rendered_output( dom: &mut VirtualDom, scope_id: ScopeId, - mut to: Option<&mut dyn WriteMutations>, + mut to: Option<&mut (dyn WriteMutations + '_)>, destroy_component_state: bool, replace_with: Option, ) { if let Some(node) = dom.scopes[scope_id.0].last_rendered_node.clone() { - node.remove_node_inner(dom, to.as_mut(), destroy_component_state, replace_with) + node.remove_node_inner( + dom, + to.as_deref_mut(), + destroy_component_state, + replace_with, + ) }; if destroy_component_state { @@ -155,7 +165,7 @@ impl + Clone, P: Clone + 'static, M: 'static> RenderD scope_id: ScopeId, new: bool, parent: Option, - mut to: Option<&mut dyn WriteMutations>, + mut to: Option<&mut (dyn WriteMutations + '_)>, ) -> usize { if new { let body = dom.run_scope_with(scope_id, || self.render()); @@ -167,24 +177,24 @@ impl + Clone, P: Clone + 'static, M: 'static> RenderD .last_rendered_node .clone() .expect("Component to be mounted"); - dom.create_scope(to.as_mut(), scope_id, new_node, parent) + dom.create_scope(to.as_deref_mut(), scope_id, new_node, parent) } fn diff( &self, dom: &mut VirtualDom, scope_id: ScopeId, - mut to: Option<&mut dyn WriteMutations>, + mut to: Option<&mut (dyn WriteMutations + '_)>, ) { let body = dom.run_scope_with(scope_id, || self.render()); - dom.diff_scope(to.as_mut(), scope_id, body); + dom.diff_scope(to.as_deref_mut(), scope_id, body); } fn remove( &self, dom: &mut VirtualDom, scope_id: ScopeId, - to: Option<&mut dyn WriteMutations>, + to: Option<&mut (dyn WriteMutations + '_)>, destroy_component_state: bool, replace_with: Option, ) { diff --git a/packages/core/src/suspense/component.rs b/packages/core/src/suspense/component.rs index 4f1eb281a0..7534b0730f 100644 --- a/packages/core/src/suspense/component.rs +++ b/packages/core/src/suspense/component.rs @@ -240,7 +240,6 @@ impl ::core::cmp::PartialEq for SuspenseBoundaryProps { /// } /// ``` #[allow(non_snake_case)] -#[cfg_attr(coverage_nightly, coverage(off))] pub fn SuspenseBoundary(__props: SuspenseBoundaryProps) -> Element { unreachable!("SuspenseBoundary should not be called directly") } @@ -311,7 +310,7 @@ impl RenderDriver for SuspenseDriver { scope_id: ScopeId, new: bool, parent: Option, - to: Option<&mut dyn WriteMutations>, + to: Option<&mut (dyn WriteMutations + '_)>, ) -> usize { if new { self.suspense_context.mount(scope_id); @@ -323,7 +322,12 @@ impl RenderDriver for SuspenseDriver { suspense_create(self, scope_id, parent, dom, to) } - fn diff(&self, dom: &mut VirtualDom, scope_id: ScopeId, to: Option<&mut dyn WriteMutations>) { + fn diff( + &self, + dom: &mut VirtualDom, + scope_id: ScopeId, + to: Option<&mut (dyn WriteMutations + '_)>, + ) { suspense_diff(self, scope_id, dom, to) } @@ -331,7 +335,7 @@ impl RenderDriver for SuspenseDriver { &self, dom: &mut VirtualDom, scope_id: ScopeId, - to: Option<&mut dyn WriteMutations>, + to: Option<&mut (dyn WriteMutations + '_)>, destroy_component_state: bool, replace_with: Option, ) { @@ -366,7 +370,7 @@ fn suspense_create( scope_id: ScopeId, parent: Option, dom: &mut VirtualDom, - mut to: Option<&mut dyn WriteMutations>, + mut to: Option<&mut (dyn WriteMutations + '_)>, ) -> usize { dom.runtime.clone().with_scope_on_stack(scope_id, || { let suspense_context = driver.context(); @@ -375,7 +379,7 @@ fn suspense_create( // First always render the children in the background. Rendering the children may cause this boundary to suspend suspense_context.under_suspense_boundary(&dom.runtime(), || { - children.create(dom, parent, Option::<&mut NoOpMutations>::None); + children.create(dom, parent, None); }); // Store the (now mounted) children back @@ -388,7 +392,7 @@ fn suspense_create( suspense_context.set_suspended_nodes(children.as_vnode().clone()); let suspense_placeholder = LastRenderedNode::new(driver.fallback().call(suspense_context.clone())); - let nodes_created = suspense_placeholder.create(dom, parent, to.as_mut()); + let nodes_created = suspense_placeholder.create(dom, parent, to.as_deref_mut()); (suspense_placeholder, nodes_created) }); @@ -398,7 +402,7 @@ fn suspense_create( // Otherwise just render the children in the real dom debug_assert!(children.mount.get().mounted()); let nodes_created = suspense_context.under_suspense_boundary(&dom.runtime(), || { - children.create(dom, parent, to.as_mut()) + children.create(dom, parent, to.as_deref_mut()) }); dom.scopes[scope_id.0].last_rendered_node = children.into(); suspense_context.take_suspended_nodes(); @@ -414,11 +418,11 @@ impl SuspenseBoundaryProps { /// Manually rerun the children of this suspense boundary without diffing against the old nodes. /// /// This should only be called by dioxus-web after the suspense boundary has been streamed in from the server. - pub fn resolve_suspense( + pub fn resolve_suspense( scope_id: ScopeId, dom: &mut VirtualDom, - to: &mut M, - only_write_templates: impl FnOnce(&mut M), + to: &mut (dyn WriteMutations + '_), + only_write_templates: impl FnOnce(&mut (dyn WriteMutations + '_)), replace_with: usize, ) { dom.runtime.clone().with_scope_on_stack(scope_id, || { @@ -453,11 +457,11 @@ impl SuspenseBoundaryProps { // Take the suspended nodes out of the suspense boundary so the children know that the boundary is not suspended while diffing let suspended = suspense_context.take_suspended_nodes(); if let Some(node) = suspended { - node.remove_node(&mut *dom, Option::<&mut NoOpMutations>::None, None); + node.remove_node(&mut *dom, None, None); } // Replace the rendered nodes with resolved nodes - currently_rendered.remove_node(&mut *dom, Some(to), Some(replace_with)); + currently_rendered.remove_node(&mut *dom, Some(&mut *to), Some(replace_with)); // Switch to only writing templates only_write_templates(to); @@ -466,7 +470,7 @@ impl SuspenseBoundaryProps { // First always render the children in the background. Rendering the children may cause this boundary to suspend suspense_context.under_suspense_boundary(&dom.runtime(), || { - children.create(dom, parent, Some(to)); + children.create(dom, parent, Some(&mut *to)); }); // Store the (now mounted) children back @@ -484,7 +488,7 @@ fn suspense_diff( driver: &SuspenseDriver, scope_id: ScopeId, dom: &mut VirtualDom, - mut to: Option<&mut dyn WriteMutations>, + mut to: Option<&mut (dyn WriteMutations + '_)>, ) { dom.runtime.clone().with_scope_on_stack(scope_id, || { let scope = &mut dom.scopes[scope_id.0]; @@ -508,7 +512,7 @@ fn suspense_diff( let new_placeholder = LastRenderedNode::new(fallback.call(suspense_context.clone())); - last_rendered_node.diff_node(&new_placeholder, dom, to.as_mut()); + last_rendered_node.diff_node(&new_placeholder, dom, to.as_deref_mut()); new_placeholder }); @@ -517,11 +521,7 @@ fn suspense_diff( // Diff the suspended nodes in the background suspense_context.under_suspense_boundary(&dom.runtime(), || { - suspended_nodes.diff_node( - &new_suspended_nodes, - dom, - Option::<&mut NoOpMutations>::None, - ); + suspended_nodes.diff_node(&new_suspended_nodes, dom, None); }); let suspense_context = @@ -534,7 +534,7 @@ fn suspense_diff( // We have no suspended nodes, and we are not suspended. Just diff the children like normal (None, false) => { suspense_context.under_suspense_boundary(&dom.runtime(), || { - last_rendered_node.diff_node(&children, dom, to.as_mut()); + last_rendered_node.diff_node(&children, dom, to.as_deref_mut()); }); // Set the last rendered node to the new children @@ -558,13 +558,13 @@ fn suspense_diff( std::slice::from_ref(&new_placeholder), parent, dom, - to.as_mut(), + to.as_deref_mut(), ); }); // Then diff the new children in the background suspense_context.under_suspense_boundary(&dom.runtime(), || { - old_children.diff_node(&new_children, dom, Option::<&mut NoOpMutations>::None); + old_children.diff_node(&new_children, dom, None); }); // Set the last rendered node to the new suspense placeholder @@ -586,11 +586,7 @@ fn suspense_diff( // First diff the two children nodes in the background suspense_context.under_suspense_boundary(&dom.runtime(), || { - old_suspended_nodes.diff_node( - &children, - dom, - Option::<&mut NoOpMutations>::None, - ); + old_suspended_nodes.diff_node(&children, dom, None); // Then replace the placeholder with the new children let mount = old_placeholder.mount.get(); @@ -599,7 +595,7 @@ fn suspense_diff( std::slice::from_ref(&children), parent, dom, - to.as_mut(), + to.as_deref_mut(), ); }); @@ -677,12 +673,7 @@ impl SuspenseContext { }; // Remove the suspended nodes if let Some(node) = scope.take_suspended_nodes() { - node.remove_node_inner( - dom, - Option::<&mut NoOpMutations>::None, - destroy_component_state, - None, - ) + node.remove_node_inner(dom, None, destroy_component_state, None) } } } diff --git a/packages/core/src/virtual_dom.rs b/packages/core/src/virtual_dom.rs index ee29115b73..b7777c6a06 100644 --- a/packages/core/src/virtual_dom.rs +++ b/packages/core/src/virtual_dom.rs @@ -573,19 +573,14 @@ impl VirtualDom { /// dom.rebuild(&mut mutations); /// ``` #[instrument(skip(self, to), level = "trace", name = "VirtualDom::rebuild")] - pub fn rebuild(&mut self, to: &mut impl WriteMutations) { + pub fn rebuild(&mut self, to: &mut (dyn WriteMutations + '_)) { let _runtime = RuntimeGuard::new(self.runtime.clone()); let driver = self.runtime.get_state(ScopeId::ROOT).render_driver(); - let m = self.runtime.clone().while_rendering(|| { - driver.create( - self, - ScopeId::ROOT, - true, - None, - Some(to as &mut dyn WriteMutations), - ) - }); + let m = self + .runtime + .clone() + .while_rendering(|| driver.create(self, ScopeId::ROOT, true, None, Some(&mut *to))); to.append_children(ElementId(0), m); } @@ -593,7 +588,7 @@ impl VirtualDom { /// Render whatever the VirtualDom has ready as fast as possible without requiring an executor to progress /// suspended subtrees. #[instrument(skip(self, to), level = "trace", name = "VirtualDom::render_immediate")] - pub fn render_immediate(&mut self, to: &mut impl WriteMutations) { + pub fn render_immediate(&mut self, to: &mut (dyn WriteMutations + '_)) { // Process any events that might be pending in the queue // Signals marked with .write() need a chance to be handled by the effect driver // This also processes futures which might progress into immediately rerunning a scope @@ -612,7 +607,7 @@ impl VirtualDom { Work::RerunScope(scope) => { // If the scope is dirty, run the scope and get the mutations self.runtime.clone().while_rendering(|| { - self.run_and_diff_scope(Some(to), scope.id); + self.run_and_diff_scope(Some(&mut *to), scope.id); }); } } @@ -723,7 +718,7 @@ impl VirtualDom { if run_scope { // If the scope is dirty, run the scope and get the mutations self.runtime.clone().while_rendering(|| { - self.run_and_diff_scope(None::<&mut NoOpMutations>, scope_id); + self.run_and_diff_scope(None, scope_id); }); tracing::trace!("Ran scope {:?} during suspense", scope_id); From 89c9e340ab57cb0045cdce53f4d4d46cfc00c27a Mon Sep 17 00:00:00 2001 From: Evan Almloff Date: Wed, 17 Jun 2026 09:05:25 -0500 Subject: [PATCH 07/16] remove refcell around suspense_location --- packages/core/src/render_driver.rs | 45 +++++++++++-------------- packages/core/src/scope_arena.rs | 5 +-- packages/core/src/scope_context.rs | 16 +++------ packages/core/src/suspense/component.rs | 31 ++++++++++------- 4 files changed, 46 insertions(+), 51 deletions(-) diff --git a/packages/core/src/render_driver.rs b/packages/core/src/render_driver.rs index c0abc23f7b..72b847104a 100644 --- a/packages/core/src/render_driver.rs +++ b/packages/core/src/render_driver.rs @@ -3,6 +3,7 @@ use std::{any::Any, cell::RefCell, panic::AssertUnwindSafe, rc::Rc}; use crate::{ ComponentFunction, Element, WriteMutations, innerlude::{CapturedPanic, ElementRef, ScopeOrder}, + scope_context::SuspenseLocation, scopes::{LastRenderedNode, ScopeId}, virtual_dom::VirtualDom, }; @@ -28,6 +29,12 @@ pub(crate) trait RenderDriver: 'static { /// A fresh instance with cloned props. fn duplicate(&self) -> Rc; + /// The suspense location to store on a newly-created scope owned by this + /// driver. + fn initial_suspense_location(&self, parent: SuspenseLocation) -> SuspenseLocation { + parent + } + /// Mount this scope's output. `to` receives DOM mutations; pass `None` for /// background rendering (e.g. suspended children). fn create( @@ -58,29 +65,6 @@ pub(crate) trait RenderDriver: 'static { ); } -/// Remove a scope's rendered output from the DOM, and drop the scope when -/// `destroy_component_state` is set. Shared by body and suspense drivers. -pub(crate) fn remove_rendered_output( - dom: &mut VirtualDom, - scope_id: ScopeId, - mut to: Option<&mut (dyn WriteMutations + '_)>, - destroy_component_state: bool, - replace_with: Option, -) { - if let Some(node) = dom.scopes[scope_id.0].last_rendered_node.clone() { - node.remove_node_inner( - dom, - to.as_deref_mut(), - destroy_component_state, - replace_with, - ) - }; - - if destroy_component_state { - dom.drop_scope(scope_id); - } -} - /// The concrete driver for plain (non-suspense) components. pub(crate) struct BodyDriver, P, M> { render_fn: F, @@ -194,10 +178,21 @@ impl + Clone, P: Clone + 'static, M: 'static> RenderD &self, dom: &mut VirtualDom, scope_id: ScopeId, - to: Option<&mut (dyn WriteMutations + '_)>, + mut to: Option<&mut (dyn WriteMutations + '_)>, destroy_component_state: bool, replace_with: Option, ) { - remove_rendered_output(dom, scope_id, to, destroy_component_state, replace_with); + if let Some(node) = dom.scopes[scope_id.0].last_rendered_node.clone() { + node.remove_node_inner( + dom, + to.as_deref_mut(), + destroy_component_state, + replace_with, + ) + }; + + if destroy_component_state { + dom.drop_scope(scope_id); + } } } diff --git a/packages/core/src/scope_arena.rs b/packages/core/src/scope_arena.rs index 3a48c543c4..ec8c436864 100644 --- a/packages/core/src/scope_arena.rs +++ b/packages/core/src/scope_arena.rs @@ -23,14 +23,15 @@ impl VirtualDom { Some(parent) => parent.height() + 1, None => 0, }; - let suspense_boundary = self + let parent_suspense_location = self .runtime .current_suspense_location() .unwrap_or(SuspenseLocation::NotSuspended); + let suspense_location = driver.initial_suspense_location(parent_suspense_location); let entry = self.scopes.vacant_entry(); let id = ScopeId(entry.key()); - let scope_runtime = Scope::new(name, id, parent_id, height, suspense_boundary, driver); + let scope_runtime = Scope::new(name, id, parent_id, height, suspense_location, driver); let reactive_context = ReactiveContext::new_for_scope(&scope_runtime, &self.runtime); let scope = entry.insert(ScopeState { diff --git a/packages/core/src/scope_context.rs b/packages/core/src/scope_context.rs index 1acccdf87d..3cb8d17973 100644 --- a/packages/core/src/scope_context.rs +++ b/packages/core/src/scope_context.rs @@ -60,7 +60,7 @@ pub(crate) struct Scope { pub(crate) after_render: RefCell>>, /// The suspense boundary location this scope is rendered under, if any. - suspense_location: RefCell, + suspense_location: SuspenseLocation, /// The driver owning this scope's rendered output. render_driver: Rc, @@ -92,7 +92,7 @@ impl Scope { status: RefCell::new(ScopeStatus::Unmounted { effects_queued: Vec::new(), }), - suspense_location: RefCell::new(suspense_location), + suspense_location, render_driver, } } @@ -123,17 +123,12 @@ impl Scope { /// Get the suspense location of this scope pub(crate) fn suspense_location(&self) -> SuspenseLocation { - self.suspense_location.borrow().clone() - } - - /// Set the suspense location for this scope - pub(crate) fn set_suspense_location(&self, location: SuspenseLocation) { - *self.suspense_location.borrow_mut() = location; + self.suspense_location.clone() } /// If this scope is a suspense boundary, return the suspense context pub(crate) fn suspense_boundary(&self) -> Option { - match &*self.suspense_location.borrow() { + match &self.suspense_location { SuspenseLocation::SuspenseBoundary(context) => Some(context.clone()), _ => None, } @@ -141,8 +136,7 @@ impl Scope { /// Check if a node should run during suspense pub(crate) fn should_run_during_suspense(&self) -> bool { - let location = self.suspense_location.borrow(); - let Some(context) = location.suspense_context() else { + let Some(context) = self.suspense_location.suspense_context() else { return false; }; diff --git a/packages/core/src/suspense/component.rs b/packages/core/src/suspense/component.rs index 7534b0730f..851c61c661 100644 --- a/packages/core/src/suspense/component.rs +++ b/packages/core/src/suspense/component.rs @@ -1,10 +1,6 @@ use std::{any::Any, cell::RefCell, rc::Rc}; -use crate::{ - innerlude::*, - render_driver::{RenderDriver, remove_rendered_output}, - scope_context::SuspenseLocation, -}; +use crate::{innerlude::*, render_driver::RenderDriver, scope_context::SuspenseLocation}; /// Properties for the [`SuspenseBoundary()`] component. #[allow(non_camel_case_types)] @@ -304,6 +300,10 @@ impl RenderDriver for SuspenseDriver { Rc::new(Self::new(self.props.borrow().clone())) } + fn initial_suspense_location(&self, _parent: SuspenseLocation) -> SuspenseLocation { + SuspenseLocation::SuspenseBoundary(self.suspense_context.clone()) + } + fn create( &self, dom: &mut VirtualDom, @@ -314,10 +314,6 @@ impl RenderDriver for SuspenseDriver { ) -> usize { if new { self.suspense_context.mount(scope_id); - let scope_state = dom.runtime.get_state(scope_id); - scope_state.set_suspense_location(SuspenseLocation::SuspenseBoundary( - self.suspense_context.clone(), - )); } suspense_create(self, scope_id, parent, dom, to) } @@ -335,16 +331,25 @@ impl RenderDriver for SuspenseDriver { &self, dom: &mut VirtualDom, scope_id: ScopeId, - to: Option<&mut (dyn WriteMutations + '_)>, + mut to: Option<&mut (dyn WriteMutations + '_)>, destroy_component_state: bool, replace_with: Option, ) { // If this is a suspense boundary, remove the suspended nodes as well SuspenseContext::remove_suspended_nodes(dom, scope_id, destroy_component_state); - // The scope's rendered output (children or fallback) is removed the - // same way a plain component's output is. - remove_rendered_output(dom, scope_id, to, destroy_component_state, replace_with); + if let Some(node) = dom.scopes[scope_id.0].last_rendered_node.clone() { + node.remove_node_inner( + dom, + to.as_deref_mut(), + destroy_component_state, + replace_with, + ) + }; + + if destroy_component_state { + dom.drop_scope(scope_id); + } } } From 23d888c265e80dc56a04c019d4fb45b7fd13fa6f Mon Sep 17 00:00:00 2001 From: Evan Almloff Date: Wed, 17 Jun 2026 09:12:54 -0500 Subject: [PATCH 08/16] leave public virtual dom api unchanged --- packages/core/src/mutations.rs | 54 -------------------------------- packages/core/src/virtual_dom.rs | 8 ++--- 2 files changed, 4 insertions(+), 58 deletions(-) diff --git a/packages/core/src/mutations.rs b/packages/core/src/mutations.rs index 9c47a3379d..eb1e485f19 100644 --- a/packages/core/src/mutations.rs +++ b/packages/core/src/mutations.rs @@ -117,60 +117,6 @@ pub trait WriteMutations { fn push_root(&mut self, id: ElementId); } -impl WriteMutations for &mut T { - fn append_children(&mut self, id: ElementId, m: usize) { - (**self).append_children(id, m) - } - fn assign_node_id(&mut self, path: &'static [u8], id: ElementId) { - (**self).assign_node_id(path, id) - } - fn create_placeholder(&mut self, id: ElementId) { - (**self).create_placeholder(id) - } - fn create_text_node(&mut self, value: &str, id: ElementId) { - (**self).create_text_node(value, id) - } - fn load_template(&mut self, template: Template, index: usize, id: ElementId) { - (**self).load_template(template, index, id) - } - fn replace_node_with(&mut self, id: ElementId, m: usize) { - (**self).replace_node_with(id, m) - } - fn replace_placeholder_with_nodes(&mut self, path: &'static [u8], m: usize) { - (**self).replace_placeholder_with_nodes(path, m) - } - fn insert_nodes_after(&mut self, id: ElementId, m: usize) { - (**self).insert_nodes_after(id, m) - } - fn insert_nodes_before(&mut self, id: ElementId, m: usize) { - (**self).insert_nodes_before(id, m) - } - fn set_attribute( - &mut self, - name: &'static str, - ns: Option<&'static str>, - value: &AttributeValue, - id: ElementId, - ) { - (**self).set_attribute(name, ns, value, id) - } - fn set_node_text(&mut self, value: &str, id: ElementId) { - (**self).set_node_text(value, id) - } - fn create_event_listener(&mut self, name: &'static str, id: ElementId) { - (**self).create_event_listener(name, id) - } - fn remove_event_listener(&mut self, name: &'static str, id: ElementId) { - (**self).remove_event_listener(name, id) - } - fn remove_node(&mut self, id: ElementId) { - (**self).remove_node(id) - } - fn push_root(&mut self, id: ElementId) { - (**self).push_root(id) - } -} - /// A `Mutation` represents a single instruction for the renderer to use to modify the UI tree to match the state /// of the Dioxus VirtualDom. /// diff --git a/packages/core/src/virtual_dom.rs b/packages/core/src/virtual_dom.rs index b7777c6a06..ccf57625f9 100644 --- a/packages/core/src/virtual_dom.rs +++ b/packages/core/src/virtual_dom.rs @@ -573,14 +573,14 @@ impl VirtualDom { /// dom.rebuild(&mut mutations); /// ``` #[instrument(skip(self, to), level = "trace", name = "VirtualDom::rebuild")] - pub fn rebuild(&mut self, to: &mut (dyn WriteMutations + '_)) { + pub fn rebuild(&mut self, to: &mut impl WriteMutations) { let _runtime = RuntimeGuard::new(self.runtime.clone()); let driver = self.runtime.get_state(ScopeId::ROOT).render_driver(); let m = self .runtime .clone() - .while_rendering(|| driver.create(self, ScopeId::ROOT, true, None, Some(&mut *to))); + .while_rendering(|| driver.create(self, ScopeId::ROOT, true, None, Some(to))); to.append_children(ElementId(0), m); } @@ -588,7 +588,7 @@ impl VirtualDom { /// Render whatever the VirtualDom has ready as fast as possible without requiring an executor to progress /// suspended subtrees. #[instrument(skip(self, to), level = "trace", name = "VirtualDom::render_immediate")] - pub fn render_immediate(&mut self, to: &mut (dyn WriteMutations + '_)) { + pub fn render_immediate(&mut self, to: &mut impl WriteMutations) { // Process any events that might be pending in the queue // Signals marked with .write() need a chance to be handled by the effect driver // This also processes futures which might progress into immediately rerunning a scope @@ -607,7 +607,7 @@ impl VirtualDom { Work::RerunScope(scope) => { // If the scope is dirty, run the scope and get the mutations self.runtime.clone().while_rendering(|| { - self.run_and_diff_scope(Some(&mut *to), scope.id); + self.run_and_diff_scope(Some(to), scope.id); }); } } From 31f8d5b55825c4064d7ba1a1f3ff31ef837c09e7 Mon Sep 17 00:00:00 2001 From: Evan Almloff Date: Wed, 17 Jun 2026 09:16:22 -0500 Subject: [PATCH 09/16] restore signature for resolve_suspense --- packages/core/src/suspense/component.rs | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/packages/core/src/suspense/component.rs b/packages/core/src/suspense/component.rs index 851c61c661..86659f4878 100644 --- a/packages/core/src/suspense/component.rs +++ b/packages/core/src/suspense/component.rs @@ -423,11 +423,11 @@ impl SuspenseBoundaryProps { /// Manually rerun the children of this suspense boundary without diffing against the old nodes. /// /// This should only be called by dioxus-web after the suspense boundary has been streamed in from the server. - pub fn resolve_suspense( + pub fn resolve_suspense( scope_id: ScopeId, dom: &mut VirtualDom, - to: &mut (dyn WriteMutations + '_), - only_write_templates: impl FnOnce(&mut (dyn WriteMutations + '_)), + to: &mut W, + only_write_templates: impl FnOnce(&mut W), replace_with: usize, ) { dom.runtime.clone().with_scope_on_stack(scope_id, || { From 64c12cfe6e2e5b0ab8806678ec53a9368ddbe032 Mon Sep 17 00:00:00 2001 From: Evan Almloff Date: Wed, 17 Jun 2026 09:30:59 -0500 Subject: [PATCH 10/16] fix clippy --- packages/core/src/render_driver.rs | 21 +++------ packages/core/src/suspense/component.rs | 57 +++++++------------------ 2 files changed, 23 insertions(+), 55 deletions(-) diff --git a/packages/core/src/render_driver.rs b/packages/core/src/render_driver.rs index 72b847104a..cd8b4e1432 100644 --- a/packages/core/src/render_driver.rs +++ b/packages/core/src/render_driver.rs @@ -2,7 +2,7 @@ use std::{any::Any, cell::RefCell, panic::AssertUnwindSafe, rc::Rc}; use crate::{ ComponentFunction, Element, WriteMutations, - innerlude::{CapturedPanic, ElementRef, ScopeOrder}, + innerlude::{CapturedPanic, ElementRef}, scope_context::SuspenseLocation, scopes::{LastRenderedNode, ScopeId}, virtual_dom::VirtualDom, @@ -149,46 +149,39 @@ impl + Clone, P: Clone + 'static, M: 'static> RenderD scope_id: ScopeId, new: bool, parent: Option, - mut to: Option<&mut (dyn WriteMutations + '_)>, + to: Option<&mut (dyn WriteMutations + '_)>, ) -> usize { if new { let body = dom.run_scope_with(scope_id, || self.render()); dom.scopes[scope_id.0].last_rendered_node = Some(LastRenderedNode::new(body)); - let height = dom.runtime.get_state(scope_id).height; - dom.dirty_scopes.remove(&ScopeOrder::new(height, scope_id)); } let new_node = dom.scopes[scope_id.0] .last_rendered_node .clone() .expect("Component to be mounted"); - dom.create_scope(to.as_deref_mut(), scope_id, new_node, parent) + dom.create_scope(to, scope_id, new_node, parent) } fn diff( &self, dom: &mut VirtualDom, scope_id: ScopeId, - mut to: Option<&mut (dyn WriteMutations + '_)>, + to: Option<&mut (dyn WriteMutations + '_)>, ) { let body = dom.run_scope_with(scope_id, || self.render()); - dom.diff_scope(to.as_deref_mut(), scope_id, body); + dom.diff_scope(to, scope_id, body); } fn remove( &self, dom: &mut VirtualDom, scope_id: ScopeId, - mut to: Option<&mut (dyn WriteMutations + '_)>, + to: Option<&mut (dyn WriteMutations + '_)>, destroy_component_state: bool, replace_with: Option, ) { if let Some(node) = dom.scopes[scope_id.0].last_rendered_node.clone() { - node.remove_node_inner( - dom, - to.as_deref_mut(), - destroy_component_state, - replace_with, - ) + node.remove_node_inner(dom, to, destroy_component_state, replace_with) }; if destroy_component_state { diff --git a/packages/core/src/suspense/component.rs b/packages/core/src/suspense/component.rs index 86659f4878..544093f082 100644 --- a/packages/core/src/suspense/component.rs +++ b/packages/core/src/suspense/component.rs @@ -331,7 +331,7 @@ impl RenderDriver for SuspenseDriver { &self, dom: &mut VirtualDom, scope_id: ScopeId, - mut to: Option<&mut (dyn WriteMutations + '_)>, + to: Option<&mut (dyn WriteMutations + '_)>, destroy_component_state: bool, replace_with: Option, ) { @@ -339,12 +339,7 @@ impl RenderDriver for SuspenseDriver { SuspenseContext::remove_suspended_nodes(dom, scope_id, destroy_component_state); if let Some(node) = dom.scopes[scope_id.0].last_rendered_node.clone() { - node.remove_node_inner( - dom, - to.as_deref_mut(), - destroy_component_state, - replace_with, - ) + node.remove_node_inner(dom, to, destroy_component_state, replace_with) }; if destroy_component_state { @@ -375,7 +370,7 @@ fn suspense_create( scope_id: ScopeId, parent: Option, dom: &mut VirtualDom, - mut to: Option<&mut (dyn WriteMutations + '_)>, + to: Option<&mut (dyn WriteMutations + '_)>, ) -> usize { dom.runtime.clone().with_scope_on_stack(scope_id, || { let suspense_context = driver.context(); @@ -397,7 +392,7 @@ fn suspense_create( suspense_context.set_suspended_nodes(children.as_vnode().clone()); let suspense_placeholder = LastRenderedNode::new(driver.fallback().call(suspense_context.clone())); - let nodes_created = suspense_placeholder.create(dom, parent, to.as_deref_mut()); + let nodes_created = suspense_placeholder.create(dom, parent, to); (suspense_placeholder, nodes_created) }); @@ -406,9 +401,8 @@ fn suspense_create( } else { // Otherwise just render the children in the real dom debug_assert!(children.mount.get().mounted()); - let nodes_created = suspense_context.under_suspense_boundary(&dom.runtime(), || { - children.create(dom, parent, to.as_deref_mut()) - }); + let nodes_created = suspense_context + .under_suspense_boundary(&dom.runtime(), || children.create(dom, parent, to)); dom.scopes[scope_id.0].last_rendered_node = children.into(); suspense_context.take_suspended_nodes(); mark_suspense_resolved(&suspense_context, dom, scope_id); @@ -451,7 +445,11 @@ impl SuspenseBoundaryProps { .parent }; - let driver = suspense_driver(dom, scope_id); + let driver_rc = dom.runtime.get_state(scope_id).render_driver(); + let driver = driver_rc + .as_any() + .downcast_ref::() + .expect("expected suspense driver on scope"); // Unmount any children to reset any scopes under this suspense boundary let children = driver.children(); @@ -493,7 +491,7 @@ fn suspense_diff( driver: &SuspenseDriver, scope_id: ScopeId, dom: &mut VirtualDom, - mut to: Option<&mut (dyn WriteMutations + '_)>, + to: Option<&mut (dyn WriteMutations + '_)>, ) { dom.runtime.clone().with_scope_on_stack(scope_id, || { let scope = &mut dom.scopes[scope_id.0]; @@ -517,7 +515,7 @@ fn suspense_diff( let new_placeholder = LastRenderedNode::new(fallback.call(suspense_context.clone())); - last_rendered_node.diff_node(&new_placeholder, dom, to.as_deref_mut()); + last_rendered_node.diff_node(&new_placeholder, dom, to); new_placeholder }); @@ -539,7 +537,7 @@ fn suspense_diff( // We have no suspended nodes, and we are not suspended. Just diff the children like normal (None, false) => { suspense_context.under_suspense_boundary(&dom.runtime(), || { - last_rendered_node.diff_node(&children, dom, to.as_deref_mut()); + last_rendered_node.diff_node(&children, dom, to); }); // Set the last rendered node to the new children @@ -563,7 +561,7 @@ fn suspense_diff( std::slice::from_ref(&new_placeholder), parent, dom, - to.as_deref_mut(), + to, ); }); @@ -596,12 +594,7 @@ fn suspense_diff( // Then replace the placeholder with the new children let mount = old_placeholder.mount.get(); let parent = dom.get_mounted_parent(mount); - old_placeholder.replace( - std::slice::from_ref(&children), - parent, - dom, - to.as_deref_mut(), - ); + old_placeholder.replace(std::slice::from_ref(&children), parent, dom, to); }); // Set the last rendered node to the new children @@ -630,24 +623,6 @@ fn un_resolve_suspense(dom: &mut VirtualDom, scope_id: ScopeId) { dom.resolved_scopes.retain(|&id| id != scope_id); } -/// Get the SuspenseDriver for a given scope. -fn suspense_driver(dom: &VirtualDom, scope_id: ScopeId) -> Rc { - let driver = dom.runtime.get_state(scope_id).render_driver(); - // Safety: we know suspense boundary scopes have SuspenseDriver as their driver. - let ptr = Rc::into_raw(driver); - // Check the downcast via as_any - unsafe { - let dyn_ref = &*ptr; - assert!( - dyn_ref.as_any().is::(), - "expected suspense driver on scope" - ); - // Reconstruct Rc from the raw pointer - let typed_ptr = ptr as *const SuspenseDriver; - Rc::from_raw(typed_ptr) - } -} - impl SuspenseContext { /// Run a closure under a suspense boundary pub(crate) fn under_suspense_boundary(&self, runtime: &Runtime, f: impl FnOnce() -> O) -> O { From 5af2e2191e54902e2c145bf6a3acac4558d557d8 Mon Sep 17 00:00:00 2001 From: Evan Almloff Date: Wed, 17 Jun 2026 09:41:06 -0500 Subject: [PATCH 11/16] revert memorize behavior for suspense to match main --- packages/core/src/suspense/component.rs | 12 ++++++++---- 1 file changed, 8 insertions(+), 4 deletions(-) diff --git a/packages/core/src/suspense/component.rs b/packages/core/src/suspense/component.rs index 544093f082..2790955c97 100644 --- a/packages/core/src/suspense/component.rs +++ b/packages/core/src/suspense/component.rs @@ -51,9 +51,13 @@ where SuspenseBoundaryProps::builder() } fn memoize(&mut self, new: &Self) -> bool { + let equal = self == new; self.fallback.__point_to(&new.fallback); - self.children = new.children.clone(); - false + if !equal { + let new_clone = new.clone(); + self.children = new_clone.children; + } + equal } } #[doc(hidden)] @@ -175,9 +179,9 @@ impl SuspenseBoundaryPropsWithOwner { /// Create a component from the props. pub fn into_vcomponent( self, - _render_fn: impl ComponentFunction, + render_fn: impl ComponentFunction, ) -> VComponent { - let component_name = std::any::type_name_of_val(&_render_fn); + let component_name = std::any::type_name_of_val(&render_fn); VComponent::new_with_driver( component_name, Rc::new(SuspenseDriver::new(self)) as Rc, From 38bb5a4db2424c411754a7eb6ac2633ca1a719cc Mon Sep 17 00:00:00 2001 From: Evan Almloff Date: Wed, 17 Jun 2026 10:02:14 -0500 Subject: [PATCH 12/16] match generic on existing resolve suspense --- packages/core/src/suspense/component.rs | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/packages/core/src/suspense/component.rs b/packages/core/src/suspense/component.rs index 2790955c97..4ef25a3592 100644 --- a/packages/core/src/suspense/component.rs +++ b/packages/core/src/suspense/component.rs @@ -421,11 +421,11 @@ impl SuspenseBoundaryProps { /// Manually rerun the children of this suspense boundary without diffing against the old nodes. /// /// This should only be called by dioxus-web after the suspense boundary has been streamed in from the server. - pub fn resolve_suspense( + pub fn resolve_suspense( scope_id: ScopeId, dom: &mut VirtualDom, - to: &mut W, - only_write_templates: impl FnOnce(&mut W), + to: &mut M, + only_write_templates: impl FnOnce(&mut M), replace_with: usize, ) { dom.runtime.clone().with_scope_on_stack(scope_id, || { From 8bb490f311270359243418cbe224c1c6119fe303 Mon Sep 17 00:00:00 2001 From: Evan Almloff Date: Wed, 17 Jun 2026 10:32:41 -0500 Subject: [PATCH 13/16] decouple prop diffing and lifecycle driver --- packages/core/src/any_props.rs | 108 ++++++++++++++++ packages/core/src/diff/component.rs | 25 ++-- packages/core/src/lib.rs | 2 + packages/core/src/nodes.rs | 41 ++++-- packages/core/src/render_driver.rs | 106 ++-------------- packages/core/src/scope_arena.rs | 25 ++-- packages/core/src/scopes.rs | 4 +- packages/core/src/suspense/component.rs | 159 ++++++++++++------------ packages/core/src/virtual_dom.rs | 25 ++-- 9 files changed, 272 insertions(+), 223 deletions(-) create mode 100644 packages/core/src/any_props.rs diff --git a/packages/core/src/any_props.rs b/packages/core/src/any_props.rs new file mode 100644 index 0000000000..346aa46566 --- /dev/null +++ b/packages/core/src/any_props.rs @@ -0,0 +1,108 @@ +use crate::{ComponentFunction, Element, innerlude::CapturedPanic}; +use std::{any::Any, panic::AssertUnwindSafe}; + +pub(crate) type BoxedAnyProps = Box; + +/// A trait for a component that can be rendered. +pub(crate) trait AnyProps: 'static { + /// Render the component with the internal props. + fn render(&self) -> Element; + /// Make the old props equal to the new type erased props. Return if the props were equal and should be memoized. + fn memoize(&mut self, other: &dyn Any) -> bool; + /// Get the props as a type erased `dyn Any`. + fn props(&self) -> &dyn Any; + /// Get the props as a type erased `dyn Any`. + fn props_mut(&mut self) -> &mut dyn Any; + /// Duplicate this component into a new boxed component. + fn duplicate(&self) -> BoxedAnyProps; +} + +/// A component along with the props the component uses to render. +pub(crate) struct VProps, P, M> { + render_fn: F, + memo: fn(&mut P, &P) -> bool, + props: P, + name: &'static str, + phantom: std::marker::PhantomData, +} + +impl, P: Clone, M> Clone for VProps { + fn clone(&self) -> Self { + Self { + render_fn: self.render_fn.clone(), + memo: self.memo, + props: self.props.clone(), + name: self.name, + phantom: std::marker::PhantomData, + } + } +} + +impl + Clone, P: Clone + 'static, M: 'static> VProps { + /// Create a [`VProps`] object. + pub fn new( + render_fn: F, + memo: fn(&mut P, &P) -> bool, + props: P, + name: &'static str, + ) -> VProps { + VProps { + render_fn, + memo, + props, + name, + phantom: std::marker::PhantomData, + } + } +} + +impl + Clone, P: Clone + 'static, M: 'static> AnyProps + for VProps +{ + fn memoize(&mut self, other: &dyn Any) -> bool { + match other.downcast_ref::

() { + Some(other) => (self.memo)(&mut self.props, other), + None => false, + } + } + + fn props(&self) -> &dyn Any { + &self.props + } + + fn props_mut(&mut self) -> &mut dyn Any { + &mut self.props + } + + fn render(&self) -> Element { + fn render_inner(_name: &str, res: Result>) -> Element { + match res { + Ok(node) => node, + Err(err) => { + #[cfg(not(target_arch = "wasm32"))] + { + tracing::error!("Panic while rendering component `{_name}`: {err:?}"); + } + Element::Err(CapturedPanic(err).into()) + } + } + } + + render_inner( + self.name, + std::panic::catch_unwind(AssertUnwindSafe(move || { + self.render_fn.rebuild(self.props.clone()) + })), + ) + } + + fn duplicate(&self) -> BoxedAnyProps { + Box::new(Self { + render_fn: self.render_fn.clone(), + memo: self.memo, + props: self.props.clone(), + name: self.name, + phantom: std::marker::PhantomData, + }) + } +} diff --git a/packages/core/src/diff/component.rs b/packages/core/src/diff/component.rs index 95c6a70083..6ad6e26750 100644 --- a/packages/core/src/diff/component.rs +++ b/packages/core/src/diff/component.rs @@ -1,4 +1,5 @@ use crate::{ + any_props::AnyProps, innerlude::{ElementRef, MountId, ScopeOrder, VComponent, WriteMutations}, nodes::VNode, scopes::{LastRenderedNode, ScopeId}, @@ -100,17 +101,19 @@ impl VNode { dom: &mut VirtualDom, to: Option<&mut (dyn WriteMutations + '_)>, ) { - // Replace components whose drivers identify different components - // (different driver type, or a different body function value) - if !old.driver.same_component(&*new.driver) { + // Replace components that have different render fns. + if old.render_fn != new.render_fn { return self.replace_vcomponent(mount, idx, new, parent, dom, to); } - // If the props are static, then we try to memoize by setting the new with the old - // The scope's driver still owns the live props, so there's no need to update anything - // This also implicitly drops the new props since they're not used - let scope_driver = dom.runtime.get_state(scope_id).render_driver(); - if scope_driver.memoize(new.driver.as_any()) { + let old_scope = &mut dom.scopes[scope_id.0]; + let old_props: &mut dyn AnyProps = old_scope.props.as_mut(); + let new_props: &dyn AnyProps = new.props.as_ref(); + + // If the props are static, then we try to memoize by setting the old props to the new props. + // The target ScopeState still has the reference to the old props, so there's no need to update + // anything else. This also implicitly drops the new props since they're not used. + if old_props.memoize(new_props.props()) { return; } @@ -159,7 +162,11 @@ impl VNode { // vcomponent. If it's already mounted, then we can just use that. if new { scope_id = dom - .new_scope(component.name, component.driver.duplicate()) + .new_scope( + component.name, + component.driver.clone(), + component.props.duplicate(), + ) .state() .id; diff --git a/packages/core/src/lib.rs b/packages/core/src/lib.rs index 5a341f0452..557b09e982 100644 --- a/packages/core/src/lib.rs +++ b/packages/core/src/lib.rs @@ -3,6 +3,7 @@ #![doc(html_favicon_url = "https://avatars.githubusercontent.com/u/79236386")] #![warn(missing_docs)] +mod any_props; mod arena; mod diff; mod effect; @@ -54,6 +55,7 @@ pub mod internal { } pub(crate) mod innerlude { + pub(crate) use crate::any_props::*; pub use crate::arena::*; pub(crate) use crate::effect::*; pub use crate::error_boundary::*; diff --git a/packages/core/src/nodes.rs b/packages/core/src/nodes.rs index 5984e40f8d..ab6c926104 100644 --- a/packages/core/src/nodes.rs +++ b/packages/core/src/nodes.rs @@ -1,15 +1,15 @@ -use std::rc::Rc; - use crate::{ Element, Event, Properties, ScopeId, VirtualDom, + any_props::BoxedAnyProps, arena::ElementId, events::ListenerCallback, - innerlude::{ElementRef, MountId, ScopeState}, + innerlude::{ElementRef, MountId, ScopeState, VProps}, properties::ComponentFunction, render_driver::{BodyDriver, RenderDriver}, }; use dioxus_core_types::DioxusFormattable; use std::ops::Deref; +use std::rc::Rc; use std::vec; use std::{ any::{Any, TypeId}, @@ -632,15 +632,23 @@ pub struct VComponent { /// The name of this component pub name: &'static str, - /// The driver owning this component's rendering lifecycle and props. + /// The raw pointer to the render function. + pub(crate) render_fn: usize, + + /// The driver owning this component's rendering lifecycle. pub(crate) driver: Rc, + + /// The props for this component. + pub(crate) props: BoxedAnyProps, } impl Clone for VComponent { fn clone(&self) -> Self { Self { name: self.name, - driver: self.driver.duplicate(), + render_fn: self.render_fn, + driver: self.driver.clone(), + props: self.props.duplicate(), } } } @@ -655,22 +663,31 @@ impl VComponent { where P: Properties + 'static, { - let driver: Rc = Rc::new(BodyDriver::new( + let render_fn = component.fn_ptr(); + let props = Box::new(VProps::new( component,

::memoize, props, fn_name, )); + let driver = BodyDriver::new(); - VComponent { - name: fn_name, - driver, - } + Self::new_with_driver(fn_name, render_fn, driver, props) } /// Create a [`VComponent`] with a custom [`RenderDriver`]. - pub(crate) fn new_with_driver(name: &'static str, driver: Rc) -> Self { - VComponent { name, driver } + pub(crate) fn new_with_driver( + name: &'static str, + render_fn: usize, + driver: impl RenderDriver, + props: BoxedAnyProps, + ) -> Self { + VComponent { + name, + render_fn, + driver: Rc::new(driver), + props, + } } /// Get the [`ScopeId`] this node is mounted to if it's mounted diff --git a/packages/core/src/render_driver.rs b/packages/core/src/render_driver.rs index cd8b4e1432..15cb928e91 100644 --- a/packages/core/src/render_driver.rs +++ b/packages/core/src/render_driver.rs @@ -1,8 +1,6 @@ -use std::{any::Any, cell::RefCell, panic::AssertUnwindSafe, rc::Rc}; - use crate::{ - ComponentFunction, Element, WriteMutations, - innerlude::{CapturedPanic, ElementRef}, + WriteMutations, + innerlude::ElementRef, scope_context::SuspenseLocation, scopes::{LastRenderedNode, ScopeId}, virtual_dom::VirtualDom, @@ -10,25 +8,11 @@ use crate::{ /// The rendering lifecycle driver for a scope. /// -/// Every scope owns exactly one driver via `Rc`: -/// - Plain components use [`BodyDriver`], which owns the component function and props. +/// Every scope owns exactly one driver: +/// - Plain components use [`BodyDriver`]. /// - Custom components may use specialized drivers, such as /// [`SuspenseDriver`](crate::suspense::SuspenseDriver). pub(crate) trait RenderDriver: 'static { - fn as_any(&self) -> &dyn Any; - - /// Whether `other` renders the same component as this driver. - fn same_component(&self, other: &dyn RenderDriver) -> bool { - self.as_any().type_id() == other.as_any().type_id() - } - - /// Update this driver's props to match `new_driver`'s. Returns `true` if - /// the props were equal (memoized). - fn memoize(&self, new_driver: &dyn Any) -> bool; - - /// A fresh instance with cloned props. - fn duplicate(&self) -> Rc; - /// The suspense location to store on a newly-created scope owned by this /// driver. fn initial_suspense_location(&self, parent: SuspenseLocation) -> SuspenseLocation { @@ -66,83 +50,15 @@ pub(crate) trait RenderDriver: 'static { } /// The concrete driver for plain (non-suspense) components. -pub(crate) struct BodyDriver, P, M> { - render_fn: F, - memo: fn(&mut P, &P) -> bool, - props: RefCell

, - name: &'static str, - phantom: std::marker::PhantomData, -} +pub(crate) struct BodyDriver; -impl + Clone, P: Clone + 'static, M: 'static> BodyDriver { - pub fn new( - render_fn: F, - memo: fn(&mut P, &P) -> bool, - props: P, - name: &'static str, - ) -> BodyDriver { - BodyDriver { - render_fn, - memo, - props: RefCell::new(props), - name, - phantom: std::marker::PhantomData, - } - } - - fn render(&self) -> Element { - fn render_inner(_name: &str, res: Result>) -> Element { - match res { - Ok(node) => node, - Err(err) => { - #[cfg(not(target_arch = "wasm32"))] - { - tracing::error!("Panic while rendering component `{_name}`: {err:?}"); - } - Element::Err(CapturedPanic(err).into()) - } - } - } - - let props = self.props.borrow().clone(); - render_inner( - self.name, - std::panic::catch_unwind(AssertUnwindSafe(move || self.render_fn.rebuild(props))), - ) +impl BodyDriver { + pub fn new() -> BodyDriver { + BodyDriver } } -impl + Clone, P: Clone + 'static, M: 'static> RenderDriver - for BodyDriver -{ - fn as_any(&self) -> &dyn Any { - self - } - - fn same_component(&self, other: &dyn RenderDriver) -> bool { - other - .as_any() - .downcast_ref::() - .is_some_and(|other| other.render_fn.fn_ptr() == self.render_fn.fn_ptr()) - } - - fn memoize(&self, new_driver: &dyn Any) -> bool { - match new_driver.downcast_ref::() { - Some(new) => (self.memo)(&mut self.props.borrow_mut(), &new.props.borrow()), - None => false, - } - } - - fn duplicate(&self) -> Rc { - Rc::new(Self { - render_fn: self.render_fn.clone(), - memo: self.memo, - props: RefCell::new(self.props.borrow().clone()), - name: self.name, - phantom: std::marker::PhantomData, - }) - } - +impl RenderDriver for BodyDriver { fn create( &self, dom: &mut VirtualDom, @@ -152,7 +68,7 @@ impl + Clone, P: Clone + 'static, M: 'static> RenderD to: Option<&mut (dyn WriteMutations + '_)>, ) -> usize { if new { - let body = dom.run_scope_with(scope_id, || self.render()); + let body = dom.run_scope(scope_id); dom.scopes[scope_id.0].last_rendered_node = Some(LastRenderedNode::new(body)); } let new_node = dom.scopes[scope_id.0] @@ -168,7 +84,7 @@ impl + Clone, P: Clone + 'static, M: 'static> RenderD scope_id: ScopeId, to: Option<&mut (dyn WriteMutations + '_)>, ) { - let body = dom.run_scope_with(scope_id, || self.render()); + let body = dom.run_scope(scope_id); dom.diff_scope(to, scope_id, body); } diff --git a/packages/core/src/scope_arena.rs b/packages/core/src/scope_arena.rs index ec8c436864..a8946d8ed1 100644 --- a/packages/core/src/scope_arena.rs +++ b/packages/core/src/scope_arena.rs @@ -1,7 +1,6 @@ -use std::rc::Rc; - use crate::{ Element, ReactiveContext, + any_props::{AnyProps, BoxedAnyProps}, innerlude::{RenderError, ScopeOrder, ScopeState}, render_driver::RenderDriver, scope_context::{Scope, SuspenseLocation}, @@ -11,12 +10,12 @@ use crate::{ impl VirtualDom { /// Create a scope rendering into the current scope's render target (the - /// root target when no scope is active). `driver` owns the scope's - /// rendering lifecycle and props. + /// root target when no scope is active). pub(super) fn new_scope( &mut self, name: &'static str, - driver: Rc, + driver: std::rc::Rc, + props: BoxedAnyProps, ) -> &mut ScopeState { let parent_id = self.runtime.try_current_scope_id(); let height = match parent_id.and_then(|id| self.runtime.try_get_state(id)) { @@ -38,6 +37,7 @@ impl VirtualDom { runtime: self.runtime.clone(), context_id: id, last_rendered_node: Default::default(), + props, reactive_context, }); @@ -46,15 +46,11 @@ impl VirtualDom { scope } - /// Run a scope's body via `render` and return the rendered nodes. This - /// will not modify the DOM or update the last rendered node of the scope. - #[tracing::instrument(skip(self, render), level = "trace", name = "VirtualDom::run_scope")] + /// Run a scope and return the rendered nodes. This will not modify the DOM + /// or update the last rendered node of the scope. + #[tracing::instrument(skip(self), level = "trace", name = "VirtualDom::run_scope")] #[track_caller] - pub(crate) fn run_scope_with( - &mut self, - scope_id: ScopeId, - render: impl FnOnce() -> Element, - ) -> Element { + pub(crate) fn run_scope(&mut self, scope_id: ScopeId) -> Element { // Ensure we are currently inside a `Runtime`. crate::Runtime::current(); @@ -70,10 +66,11 @@ impl VirtualDom { pre_run(); } + let props: &dyn AnyProps = &*scope.props; let span = tracing::trace_span!("render", scope = %scope.state().name); span.in_scope(|| { scope.reactive_context.reset_and_run_in(|| { - let render_return = render(); + let render_return = props.render(); // After the component is run, we need to do a deep clone of the VNode. This // breaks any references to mounted parts of the VNode from the component. // Without this, the component could store a mounted version of the VNode diff --git a/packages/core/src/scopes.rs b/packages/core/src/scopes.rs index ecf0ff1f6d..643bc5e20c 100644 --- a/packages/core/src/scopes.rs +++ b/packages/core/src/scopes.rs @@ -1,5 +1,6 @@ use crate::{ - Element, RenderError, Runtime, VNode, reactive_context::ReactiveContext, scope_context::Scope, + Element, RenderError, Runtime, VNode, any_props::BoxedAnyProps, + reactive_context::ReactiveContext, scope_context::Scope, }; use std::{cell::Ref, rc::Rc}; @@ -75,6 +76,7 @@ pub struct ScopeState { /// The last node that has been rendered for this component. This node may not be mounted. /// During suspense, this component can be rendered in the background multiple times. pub(crate) last_rendered_node: Option, + pub(crate) props: BoxedAnyProps, pub(crate) reactive_context: ReactiveContext, } diff --git a/packages/core/src/suspense/component.rs b/packages/core/src/suspense/component.rs index 4ef25a3592..2b75d55c57 100644 --- a/packages/core/src/suspense/component.rs +++ b/packages/core/src/suspense/component.rs @@ -1,5 +1,3 @@ -use std::{any::Any, cell::RefCell, rc::Rc}; - use crate::{innerlude::*, render_driver::RenderDriver, scope_context::SuspenseLocation}; /// Properties for the [`SuspenseBoundary()`] component. @@ -182,9 +180,18 @@ impl SuspenseBoundaryPropsWithOwner { render_fn: impl ComponentFunction, ) -> VComponent { let component_name = std::any::type_name_of_val(&render_fn); + let render_fn_ptr = render_fn.fn_ptr(); + let props = VProps::new( + move |wrapper: Self| render_fn.rebuild(wrapper.inner), + ::memoize, + self, + component_name, + ); VComponent::new_with_driver( component_name, - Rc::new(SuspenseDriver::new(self)) as Rc, + render_fn_ptr, + SuspenseDriver::new(), + Box::new(props), ) } } @@ -246,21 +253,18 @@ pub fn SuspenseBoundary(__props: SuspenseBoundaryProps) -> Element { /// The rendering lifecycle of a suspense boundary scope. /// -/// The driver owns the [`SuspenseContext`] for this boundary and the -/// children/fallback props. Children render in the background first; the +/// The driver owns the [`SuspenseContext`] for this boundary. Children render in the background first; the /// scope's visible output is either the children or the fallback depending on /// whether any descendant is suspended. pub(crate) struct SuspenseDriver { /// The suspense context owned by this boundary. suspense_context: SuspenseContext, - props: RefCell, } impl SuspenseDriver { - fn new(props: SuspenseBoundaryPropsWithOwner) -> Self { + fn new() -> Self { Self { suspense_context: SuspenseContext::new(), - props: RefCell::new(props), } } @@ -268,42 +272,9 @@ impl SuspenseDriver { pub(crate) fn context(&self) -> SuspenseContext { self.suspense_context.clone() } - - fn children(&self) -> LastRenderedNode { - self.props.borrow().inner.children.clone() - } - - fn fallback(&self) -> Callback { - self.props.borrow().inner.fallback - } - - fn store_children(&self, children: &LastRenderedNode) { - self.props.borrow_mut().inner.children.clone_from(children); - } } impl RenderDriver for SuspenseDriver { - fn as_any(&self) -> &dyn Any { - self - } - - fn same_component(&self, other: &dyn RenderDriver) -> bool { - other.as_any().downcast_ref::().is_some() - } - - fn memoize(&self, new_driver: &dyn Any) -> bool { - match new_driver.downcast_ref::() { - Some(other) => { - Properties::memoize(&mut *self.props.borrow_mut(), &other.props.borrow()) - } - None => false, - } - } - - fn duplicate(&self) -> Rc { - Rc::new(Self::new(self.props.borrow().clone())) - } - fn initial_suspense_location(&self, _parent: SuspenseLocation) -> SuspenseLocation { SuspenseLocation::SuspenseBoundary(self.suspense_context.clone()) } @@ -379,7 +350,7 @@ fn suspense_create( dom.runtime.clone().with_scope_on_stack(scope_id, || { let suspense_context = driver.context(); - let children = driver.children(); + let children = suspense_children(scope_id, dom); // First always render the children in the background. Rendering the children may cause this boundary to suspend suspense_context.under_suspense_boundary(&dom.runtime(), || { @@ -387,15 +358,16 @@ fn suspense_create( }); // Store the (now mounted) children back - driver.store_children(&children); + store_suspense_children(scope_id, dom, &children); // If there are suspended futures, render the fallback if !suspense_context.suspended_futures().is_empty() { let (node, nodes_created) = suspense_context.in_suspense_placeholder(&dom.runtime(), || { suspense_context.set_suspended_nodes(children.as_vnode().clone()); - let suspense_placeholder = - LastRenderedNode::new(driver.fallback().call(suspense_context.clone())); + let suspense_placeholder = LastRenderedNode::new( + suspense_fallback(scope_id, dom).call(suspense_context.clone()), + ); let nodes_created = suspense_placeholder.create(dom, parent, to); (suspense_placeholder, nodes_created) }); @@ -416,7 +388,33 @@ fn suspense_create( }) } +fn suspense_props(scope_id: ScopeId, dom: &mut VirtualDom) -> &mut SuspenseBoundaryProps { + SuspenseBoundaryProps::downcast_from_props(dom.scopes[scope_id.0].props.as_mut()) + .expect("expected suspense props on suspense boundary scope") +} + +fn suspense_children(scope_id: ScopeId, dom: &mut VirtualDom) -> LastRenderedNode { + suspense_props(scope_id, dom).children.clone() +} + +fn suspense_fallback( + scope_id: ScopeId, + dom: &mut VirtualDom, +) -> Callback { + suspense_props(scope_id, dom).fallback +} + +fn store_suspense_children(scope_id: ScopeId, dom: &mut VirtualDom, children: &LastRenderedNode) { + suspense_props(scope_id, dom).children.clone_from(children); +} + impl SuspenseBoundaryProps { + /// Try to downcast [`AnyProps`] to [`SuspenseBoundaryProps`]. + pub(crate) fn downcast_from_props(props: &mut dyn AnyProps) -> Option<&mut Self> { + let inner: Option<&mut SuspenseBoundaryPropsWithOwner> = props.props_mut().downcast_mut(); + inner.map(|inner| &mut inner.inner) + } + #[doc(hidden)] /// Manually rerun the children of this suspense boundary without diffing against the old nodes. /// @@ -430,33 +428,31 @@ impl SuspenseBoundaryProps { ) { dom.runtime.clone().with_scope_on_stack(scope_id, || { let _runtime = RuntimeGuard::new(dom.runtime()); - let Some(scope_state) = dom.scopes.get_mut(scope_id.0) else { - return; + let (currently_rendered, parent) = { + let Some(scope_state) = dom.scopes.get_mut(scope_id.0) else { + return; + }; + + // Reset the suspense context + let suspense_context = scope_state.state().suspense_boundary().unwrap().clone(); + suspense_context.inner.suspended_tasks.borrow_mut().clear(); + + // Get the parent of the suspense boundary to later create children with the right parent + let currently_rendered = scope_state.last_rendered_node.clone().unwrap(); + let mount = currently_rendered.mount.get(); + let parent = { + let mounts = dom.runtime.mounts.borrow(); + mounts + .get(mount.0) + .expect("suspense placeholder is not mounted") + .parent + }; + + (currently_rendered, parent) }; - // Reset the suspense context - let suspense_context = scope_state.state().suspense_boundary().unwrap().clone(); - suspense_context.inner.suspended_tasks.borrow_mut().clear(); - - // Get the parent of the suspense boundary to later create children with the right parent - let currently_rendered = scope_state.last_rendered_node.clone().unwrap(); - let mount = currently_rendered.mount.get(); - let parent = { - let mounts = dom.runtime.mounts.borrow(); - mounts - .get(mount.0) - .expect("suspense placeholder is not mounted") - .parent - }; - - let driver_rc = dom.runtime.get_state(scope_id).render_driver(); - let driver = driver_rc - .as_any() - .downcast_ref::() - .expect("expected suspense driver on scope"); - // Unmount any children to reset any scopes under this suspense boundary - let children = driver.children(); + let children = suspense_children(scope_id, dom); let suspense_context = SuspenseContext::downcast_suspense_boundary_from_scope(&dom.runtime, scope_id) .unwrap(); @@ -481,7 +477,7 @@ impl SuspenseBoundaryProps { }); // Store the (now mounted) children back - driver.store_children(&children); + store_suspense_children(scope_id, dom, &children); dom.scopes[scope_id.0].last_rendered_node = Some(children); // Run any closures that were waiting for the suspense to resolve @@ -492,19 +488,22 @@ impl SuspenseBoundaryProps { /// Diff a suspense boundary scope against its current children/fallback props. fn suspense_diff( - driver: &SuspenseDriver, + _driver: &SuspenseDriver, scope_id: ScopeId, dom: &mut VirtualDom, to: Option<&mut (dyn WriteMutations + '_)>, ) { dom.runtime.clone().with_scope_on_stack(scope_id, || { - let scope = &mut dom.scopes[scope_id.0]; - let last_rendered_node = scope.last_rendered_node.clone().unwrap(); + let last_rendered_node = dom.scopes[scope_id.0].last_rendered_node.clone().unwrap(); - let children = driver.children(); - let fallback = driver.fallback(); + let children = suspense_children(scope_id, dom); + let fallback = suspense_fallback(scope_id, dom); - let suspense_context = scope.state().suspense_boundary().unwrap().clone(); + let suspense_context = dom.scopes[scope_id.0] + .state() + .suspense_boundary() + .unwrap() + .clone(); let suspended_nodes = suspense_context.suspended_nodes(); let suspended = !suspense_context.suspended_futures().is_empty(); match (suspended_nodes, suspended) { @@ -536,7 +535,7 @@ fn suspense_diff( .unwrap(); suspense_context.set_suspended_nodes(new_suspended_nodes); - driver.store_children(&children); + store_suspense_children(scope_id, dom, &children); } // We have no suspended nodes, and we are not suspended. Just diff the children like normal (None, false) => { @@ -545,7 +544,7 @@ fn suspense_diff( }); // Set the last rendered node to the new children - driver.store_children(&children); + store_suspense_children(scope_id, dom, &children); dom.scopes[scope_id.0].last_rendered_node = Some(children); } // We have no suspended nodes, but we just became suspended. Move the children to the background @@ -582,7 +581,7 @@ fn suspense_diff( .unwrap(); suspense_context.set_suspended_nodes(new_children); - driver.store_children(&children); + store_suspense_children(scope_id, dom, &children); un_resolve_suspense(dom, scope_id); } // We have suspended nodes, but we just got out of suspense. Move the suspended nodes to the foreground @@ -602,7 +601,7 @@ fn suspense_diff( }); // Set the last rendered node to the new children - driver.store_children(&children); + store_suspense_children(scope_id, dom, &children); dom.scopes[scope_id.0].last_rendered_node = Some(children); mark_suspense_resolved(&suspense_context, dom, scope_id); diff --git a/packages/core/src/virtual_dom.rs b/packages/core/src/virtual_dom.rs index ccf57625f9..0d0c4b15bf 100644 --- a/packages/core/src/virtual_dom.rs +++ b/packages/core/src/virtual_dom.rs @@ -4,12 +4,12 @@ use crate::innerlude::Work; use crate::properties::RootProps; -use crate::render_driver::{BodyDriver, RenderDriver}; +use crate::render_driver::BodyDriver; use crate::root_wrapper::RootScopeWrapper; use crate::{ ComponentFunction, Element, Mutations, arena::ElementId, - innerlude::{NoOpMutations, SchedulerMsg, ScopeOrder, ScopeState, WriteMutations}, + innerlude::{NoOpMutations, SchedulerMsg, ScopeOrder, ScopeState, VProps, WriteMutations}, runtime::{Runtime, RuntimeGuard}, scopes::ScopeId, }; @@ -288,9 +288,15 @@ impl VirtualDom { root: impl ComponentFunction, root_props: P, ) -> Self { - let driver: Rc = - Rc::new(BodyDriver::new(root, |_, _| true, root_props, "Root")); - Self::new_with_component(VComponent::new_with_driver("root", driver)) + let render_fn = root.fn_ptr(); + let props = VProps::new(root, |_, _| true, root_props, "Root"); + let driver = BodyDriver::new(); + Self::new_with_component(VComponent::new_with_driver( + "root", + render_fn, + driver, + Box::new(props), + )) } /// Create a new virtualdom and build it immediately @@ -313,13 +319,8 @@ impl VirtualDom { resolved_scopes: Default::default(), }; - let root_driver: Rc = Rc::new(BodyDriver::new( - RootScopeWrapper, - |_, _| true, - RootProps(root), - "RootWrapper", - )); - dom.new_scope("app", root_driver); + let root = VComponent::new(RootScopeWrapper, RootProps(root), "RootWrapper"); + dom.new_scope("app", root.driver.clone(), root.props.duplicate()); #[cfg(debug_assertions)] dom.register_subsecond_handler(); From 0edbf9366e7faeadcc77e4059291bf952b60abe6 Mon Sep 17 00:00:00 2001 From: Evan Almloff Date: Wed, 17 Jun 2026 10:36:07 -0500 Subject: [PATCH 14/16] clean up diff --- packages/core/src/any_props.rs | 2 ++ packages/core/src/diff/component.rs | 19 +++++++++++-------- packages/core/src/nodes.rs | 6 +++--- packages/core/src/scope_arena.rs | 20 ++++++++------------ packages/core/src/scopes.rs | 4 ++-- packages/core/src/suspense/component.rs | 6 ++++-- 6 files changed, 30 insertions(+), 27 deletions(-) diff --git a/packages/core/src/any_props.rs b/packages/core/src/any_props.rs index 346aa46566..2ad9b5a3ab 100644 --- a/packages/core/src/any_props.rs +++ b/packages/core/src/any_props.rs @@ -79,6 +79,8 @@ impl + Clone, P: Clone + 'static, M: 'static> AnyProp match res { Ok(node) => node, Err(err) => { + // on wasm this massively bloats binary sizes and we can't even capture the panic + // so do nothing #[cfg(not(target_arch = "wasm32"))] { tracing::error!("Panic while rendering component `{_name}`: {err:?}"); diff --git a/packages/core/src/diff/component.rs b/packages/core/src/diff/component.rs index 6ad6e26750..5a5e137609 100644 --- a/packages/core/src/diff/component.rs +++ b/packages/core/src/diff/component.rs @@ -1,3 +1,5 @@ +use std::ops::{Deref, DerefMut}; + use crate::{ any_props::AnyProps, innerlude::{ElementRef, MountId, ScopeOrder, VComponent, WriteMutations}, @@ -101,19 +103,21 @@ impl VNode { dom: &mut VirtualDom, to: Option<&mut (dyn WriteMutations + '_)>, ) { - // Replace components that have different render fns. + // Replace components that have different render fns if old.render_fn != new.render_fn { return self.replace_vcomponent(mount, idx, new, parent, dom, to); } + // copy out the box for both let old_scope = &mut dom.scopes[scope_id.0]; - let old_props: &mut dyn AnyProps = old_scope.props.as_mut(); - let new_props: &dyn AnyProps = new.props.as_ref(); + let old_props: &mut dyn AnyProps = old_scope.props.deref_mut(); + let new_props: &dyn AnyProps = new.props.deref(); - // If the props are static, then we try to memoize by setting the old props to the new props. - // The target ScopeState still has the reference to the old props, so there's no need to update - // anything else. This also implicitly drops the new props since they're not used. + // If the props are static, then we try to memoize by setting the new with the old + // The target ScopeState still has the reference to the old props, so there's no need to update anything + // This also implicitly drops the new props since they're not used if old_props.memoize(new_props.props()) { + tracing::trace!("Memoized props for component {:#?}", scope_id,); return; } @@ -158,8 +162,7 @@ impl VNode { let mut scope_id = ScopeId(dom.get_mounted_dyn_node(mount, idx)); let new = scope_id.is_placeholder(); - // If the scope id is a placeholder, we need to load up a new scope for this - // vcomponent. If it's already mounted, then we can just use that. + // If the scopeid is a placeholder, we need to load up a new scope for this vcomponent. If it's already mounted, then we can just use that if new { scope_id = dom .new_scope( diff --git a/packages/core/src/nodes.rs b/packages/core/src/nodes.rs index ab6c926104..2bf0963a5a 100644 --- a/packages/core/src/nodes.rs +++ b/packages/core/src/nodes.rs @@ -632,13 +632,13 @@ pub struct VComponent { /// The name of this component pub name: &'static str, - /// The raw pointer to the render function. + /// The raw pointer to the render function pub(crate) render_fn: usize, /// The driver owning this component's rendering lifecycle. pub(crate) driver: Rc, - /// The props for this component. + /// The props for this component pub(crate) props: BoxedAnyProps, } @@ -646,9 +646,9 @@ impl Clone for VComponent { fn clone(&self) -> Self { Self { name: self.name, + props: self.props.duplicate(), render_fn: self.render_fn, driver: self.driver.clone(), - props: self.props.duplicate(), } } } diff --git a/packages/core/src/scope_arena.rs b/packages/core/src/scope_arena.rs index a8946d8ed1..c90450a69d 100644 --- a/packages/core/src/scope_arena.rs +++ b/packages/core/src/scope_arena.rs @@ -9,8 +9,6 @@ use crate::{ }; impl VirtualDom { - /// Create a scope rendering into the current scope's render target (the - /// root target when no scope is active). pub(super) fn new_scope( &mut self, name: &'static str, @@ -36,8 +34,8 @@ impl VirtualDom { let scope = entry.insert(ScopeState { runtime: self.runtime.clone(), context_id: id, - last_rendered_node: Default::default(), props, + last_rendered_node: Default::default(), reactive_context, }); @@ -46,8 +44,7 @@ impl VirtualDom { scope } - /// Run a scope and return the rendered nodes. This will not modify the DOM - /// or update the last rendered node of the scope. + /// Run a scope and return the rendered nodes. This will not modify the DOM or update the last rendered node of the scope. #[tracing::instrument(skip(self), level = "trace", name = "VirtualDom::run_scope")] #[track_caller] pub(crate) fn run_scope(&mut self, scope_id: ScopeId) -> Element { @@ -67,6 +64,7 @@ impl VirtualDom { } let props: &dyn AnyProps = &*scope.props; + let span = tracing::trace_span!("render", scope = %scope.state().name); span.in_scope(|| { scope.reactive_context.reset_and_run_in(|| { @@ -95,18 +93,16 @@ impl VirtualDom { }) }; - { - let scope_state = scope.state(); + let scope_state = scope.state(); - // Run all post-render hooks - for post_run in scope_state.after_render.borrow_mut().iter_mut() { - post_run(); - } + // Run all post-render hooks + for post_run in scope_state.after_render.borrow_mut().iter_mut() { + post_run(); } // remove this scope from dirty scopes self.dirty_scopes - .remove(&ScopeOrder::new(scope.state().height, scope_id)); + .remove(&ScopeOrder::new(scope_state.height, scope_id)); output }) } diff --git a/packages/core/src/scopes.rs b/packages/core/src/scopes.rs index 643bc5e20c..014148cef7 100644 --- a/packages/core/src/scopes.rs +++ b/packages/core/src/scopes.rs @@ -73,8 +73,8 @@ impl ScopeId { pub struct ScopeState { pub(crate) runtime: Rc, pub(crate) context_id: ScopeId, - /// The last node that has been rendered for this component. This node may not be mounted. - /// During suspense, this component can be rendered in the background multiple times. + /// The last node that has been rendered for this component. This node may not ben mounted + /// During suspense, this component can be rendered in the background multiple times pub(crate) last_rendered_node: Option, pub(crate) props: BoxedAnyProps, pub(crate) reactive_context: ReactiveContext, diff --git a/packages/core/src/suspense/component.rs b/packages/core/src/suspense/component.rs index 2b75d55c57..da32346bcc 100644 --- a/packages/core/src/suspense/component.rs +++ b/packages/core/src/suspense/component.rs @@ -247,7 +247,7 @@ impl ::core::cmp::PartialEq for SuspenseBoundaryProps { /// } /// ``` #[allow(non_snake_case)] -pub fn SuspenseBoundary(__props: SuspenseBoundaryProps) -> Element { +pub fn SuspenseBoundary(mut __props: SuspenseBoundaryProps) -> Element { unreachable!("SuspenseBoundary should not be called directly") } @@ -642,7 +642,9 @@ impl SuspenseContext { runtime: &Runtime, scope_id: ScopeId, ) -> Option { - runtime.try_get_state(scope_id)?.suspense_boundary() + runtime + .try_get_state(scope_id) + .and_then(|scope| scope.suspense_boundary()) } pub(crate) fn remove_suspended_nodes( From ca99bf90612ca628d054576d2adea4f0968c9693 Mon Sep 17 00:00:00 2001 From: Evan Almloff Date: Wed, 17 Jun 2026 10:40:02 -0500 Subject: [PATCH 15/16] undo rename --- packages/core/src/scope_context.rs | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/packages/core/src/scope_context.rs b/packages/core/src/scope_context.rs index 3cb8d17973..1f22b36852 100644 --- a/packages/core/src/scope_context.rs +++ b/packages/core/src/scope_context.rs @@ -59,8 +59,8 @@ pub(crate) struct Scope { pub(crate) before_render: RefCell>>, pub(crate) after_render: RefCell>>, - /// The suspense boundary location this scope is rendered under, if any. - suspense_location: SuspenseLocation, + /// The suspense boundary that this scope is currently in (if any) + suspense_boundary: SuspenseLocation, /// The driver owning this scope's rendered output. render_driver: Rc, @@ -74,7 +74,7 @@ impl Scope { id: ScopeId, parent_id: Option, height: u32, - suspense_location: SuspenseLocation, + suspense_boundary: SuspenseLocation, render_driver: Rc, ) -> Self { Self { @@ -92,7 +92,7 @@ impl Scope { status: RefCell::new(ScopeStatus::Unmounted { effects_queued: Vec::new(), }), - suspense_location, + suspense_boundary, render_driver, } } @@ -123,20 +123,20 @@ impl Scope { /// Get the suspense location of this scope pub(crate) fn suspense_location(&self) -> SuspenseLocation { - self.suspense_location.clone() + self.suspense_boundary.clone() } /// If this scope is a suspense boundary, return the suspense context pub(crate) fn suspense_boundary(&self) -> Option { - match &self.suspense_location { - SuspenseLocation::SuspenseBoundary(context) => Some(context.clone()), + match self.suspense_location() { + SuspenseLocation::SuspenseBoundary(context) => Some(context), _ => None, } } /// Check if a node should run during suspense pub(crate) fn should_run_during_suspense(&self) -> bool { - let Some(context) = self.suspense_location.suspense_context() else { + let Some(context) = self.suspense_boundary.suspense_context() else { return false; }; From e2eedb6396a9d702f060e8a1f15c447ca8d3d4d2 Mon Sep 17 00:00:00 2001 From: Evan Almloff Date: Wed, 17 Jun 2026 10:51:03 -0500 Subject: [PATCH 16/16] clean up suspense component diff --- packages/core/src/suspense/component.rs | 478 ++++++++++++------------ 1 file changed, 245 insertions(+), 233 deletions(-) diff --git a/packages/core/src/suspense/component.rs b/packages/core/src/suspense/component.rs index da32346bcc..fa5734a2c6 100644 --- a/packages/core/src/suspense/component.rs +++ b/packages/core/src/suspense/component.rs @@ -250,12 +250,24 @@ impl ::core::cmp::PartialEq for SuspenseBoundaryProps { pub fn SuspenseBoundary(mut __props: SuspenseBoundaryProps) -> Element { unreachable!("SuspenseBoundary should not be called directly") } +#[allow(non_snake_case)] +#[doc(hidden)] +mod SuspenseBoundary_completions { + #[doc(hidden)] + #[allow(non_camel_case_types)] + /// This enum is generated to help autocomplete the braces after the component. It does nothing + pub enum Component { + SuspenseBoundary {}, + } +} +#[allow(unused)] +pub use SuspenseBoundary_completions::Component::SuspenseBoundary; +use generational_box::Owner; -/// The rendering lifecycle of a suspense boundary scope. +/// The rendering lifecycle driver for a suspense boundary scope. /// -/// The driver owns the [`SuspenseContext`] for this boundary. Children render in the background first; the -/// scope's visible output is either the children or the fallback depending on -/// whether any descendant is suspended. +/// The driver owns the [`SuspenseContext`] for this boundary and delegates the actual +/// create/diff/remove logic to the [`SuspenseBoundaryProps`] methods below. pub(crate) struct SuspenseDriver { /// The suspense context owned by this boundary. suspense_context: SuspenseContext, @@ -267,11 +279,6 @@ impl SuspenseDriver { suspense_context: SuspenseContext::new(), } } - - /// Get the suspense context for this boundary. - pub(crate) fn context(&self) -> SuspenseContext { - self.suspense_context.clone() - } } impl RenderDriver for SuspenseDriver { @@ -290,7 +297,7 @@ impl RenderDriver for SuspenseDriver { if new { self.suspense_context.mount(scope_id); } - suspense_create(self, scope_id, parent, dom, to) + SuspenseBoundaryProps::create(scope_id, parent, dom, to) } fn diff( @@ -299,7 +306,7 @@ impl RenderDriver for SuspenseDriver { scope_id: ScopeId, to: Option<&mut (dyn WriteMutations + '_)>, ) { - suspense_diff(self, scope_id, dom, to) + SuspenseBoundaryProps::diff(scope_id, dom, to) } fn remove( @@ -323,96 +330,87 @@ impl RenderDriver for SuspenseDriver { } } -#[allow(non_snake_case)] -#[doc(hidden)] -mod SuspenseBoundary_completions { - #[doc(hidden)] - #[allow(non_camel_case_types)] - /// This enum is generated to help autocomplete the braces after the component. It does nothing - pub enum Component { - SuspenseBoundary {}, +/// Suspense has a custom diffing algorithm that diffs the suspended nodes in the background without rendering them +impl SuspenseBoundaryProps { + /// Try to downcast [`AnyProps`] to [`SuspenseBoundaryProps`] + pub(crate) fn downcast_from_props(props: &mut dyn AnyProps) -> Option<&mut Self> { + let inner: Option<&mut SuspenseBoundaryPropsWithOwner> = props.props_mut().downcast_mut(); + inner.map(|inner| &mut inner.inner) } -} -#[allow(unused)] -pub use SuspenseBoundary_completions::Component::SuspenseBoundary; -use generational_box::Owner; -/// Mount a suspense boundary scope: render the children in the background -/// first, then mount either the children or the fallback depending on whether -/// anything suspended. -fn suspense_create( - driver: &SuspenseDriver, - scope_id: ScopeId, - parent: Option, - dom: &mut VirtualDom, - to: Option<&mut (dyn WriteMutations + '_)>, -) -> usize { - dom.runtime.clone().with_scope_on_stack(scope_id, || { - let suspense_context = driver.context(); - - let children = suspense_children(scope_id, dom); - - // First always render the children in the background. Rendering the children may cause this boundary to suspend - suspense_context.under_suspense_boundary(&dom.runtime(), || { - children.create(dom, parent, None); - }); - - // Store the (now mounted) children back - store_suspense_children(scope_id, dom, &children); - - // If there are suspended futures, render the fallback - if !suspense_context.suspended_futures().is_empty() { - let (node, nodes_created) = - suspense_context.in_suspense_placeholder(&dom.runtime(), || { - suspense_context.set_suspended_nodes(children.as_vnode().clone()); - let suspense_placeholder = LastRenderedNode::new( - suspense_fallback(scope_id, dom).call(suspense_context.clone()), - ); - let nodes_created = suspense_placeholder.create(dom, parent, to); - (suspense_placeholder, nodes_created) - }); - - dom.scopes[scope_id.0].last_rendered_node = Some(node); - nodes_created - } else { - // Otherwise just render the children in the real dom - debug_assert!(children.mount.get().mounted()); - let nodes_created = suspense_context - .under_suspense_boundary(&dom.runtime(), || children.create(dom, parent, to)); - dom.scopes[scope_id.0].last_rendered_node = children.into(); - suspense_context.take_suspended_nodes(); - mark_suspense_resolved(&suspense_context, dom, scope_id); - - nodes_created - } - }) -} + pub(crate) fn create( + scope_id: ScopeId, + parent: Option, + dom: &mut VirtualDom, + to: Option<&mut (dyn WriteMutations + '_)>, + ) -> usize { + dom.runtime.clone().with_scope_on_stack(scope_id, || { + let scope_state = &mut dom.scopes[scope_id.0]; + let props = Self::downcast_from_props(&mut *scope_state.props).unwrap(); + let suspense_context = + SuspenseContext::downcast_suspense_boundary_from_scope(&dom.runtime, scope_id) + .unwrap(); -fn suspense_props(scope_id: ScopeId, dom: &mut VirtualDom) -> &mut SuspenseBoundaryProps { - SuspenseBoundaryProps::downcast_from_props(dom.scopes[scope_id.0].props.as_mut()) - .expect("expected suspense props on suspense boundary scope") -} + let children = props.children.clone(); -fn suspense_children(scope_id: ScopeId, dom: &mut VirtualDom) -> LastRenderedNode { - suspense_props(scope_id, dom).children.clone() -} + // First always render the children in the background. Rendering the children may cause this boundary to suspend + suspense_context.under_suspense_boundary(&dom.runtime(), || { + children.create(dom, parent, None); + }); -fn suspense_fallback( - scope_id: ScopeId, - dom: &mut VirtualDom, -) -> Callback { - suspense_props(scope_id, dom).fallback -} + // Store the (now mounted) children back into the scope state + let scope_state = &mut dom.scopes[scope_id.0]; + let props = Self::downcast_from_props(&mut *scope_state.props).unwrap(); + props.children.clone_from(&children); -fn store_suspense_children(scope_id: ScopeId, dom: &mut VirtualDom, children: &LastRenderedNode) { - suspense_props(scope_id, dom).children.clone_from(children); -} + let scope_state = &mut dom.scopes[scope_id.0]; + let suspense_context = scope_state + .state() + .suspense_location() + .suspense_context() + .unwrap() + .clone(); -impl SuspenseBoundaryProps { - /// Try to downcast [`AnyProps`] to [`SuspenseBoundaryProps`]. - pub(crate) fn downcast_from_props(props: &mut dyn AnyProps) -> Option<&mut Self> { - let inner: Option<&mut SuspenseBoundaryPropsWithOwner> = props.props_mut().downcast_mut(); - inner.map(|inner| &mut inner.inner) + // If there are suspended futures, render the fallback + + if !suspense_context.suspended_futures().is_empty() { + let (node, nodes_created) = + suspense_context.in_suspense_placeholder(&dom.runtime(), || { + let scope_state = &mut dom.scopes[scope_id.0]; + let props = Self::downcast_from_props(&mut *scope_state.props).unwrap(); + let suspense_context = + SuspenseContext::downcast_suspense_boundary_from_scope( + &dom.runtime, + scope_id, + ) + .unwrap(); + suspense_context.set_suspended_nodes(children.as_vnode().clone()); + let suspense_placeholder = + LastRenderedNode::new(props.fallback.call(suspense_context)); + let nodes_created = suspense_placeholder.create(dom, parent, to); + (suspense_placeholder, nodes_created) + }); + + let scope_state = &mut dom.scopes[scope_id.0]; + scope_state.last_rendered_node = Some(node); + + nodes_created + } else { + // Otherwise just render the children in the real dom + debug_assert!(children.mount.get().mounted()); + let nodes_created = suspense_context + .under_suspense_boundary(&dom.runtime(), || children.create(dom, parent, to)); + let scope_state = &mut dom.scopes[scope_id.0]; + scope_state.last_rendered_node = children.into(); + let suspense_context = + SuspenseContext::downcast_suspense_boundary_from_scope(&dom.runtime, scope_id) + .unwrap(); + suspense_context.take_suspended_nodes(); + mark_suspense_resolved(&suspense_context, dom, scope_id); + + nodes_created + } + }) } #[doc(hidden)] @@ -428,31 +426,34 @@ impl SuspenseBoundaryProps { ) { dom.runtime.clone().with_scope_on_stack(scope_id, || { let _runtime = RuntimeGuard::new(dom.runtime()); - let (currently_rendered, parent) = { - let Some(scope_state) = dom.scopes.get_mut(scope_id.0) else { - return; - }; - - // Reset the suspense context - let suspense_context = scope_state.state().suspense_boundary().unwrap().clone(); - suspense_context.inner.suspended_tasks.borrow_mut().clear(); - - // Get the parent of the suspense boundary to later create children with the right parent - let currently_rendered = scope_state.last_rendered_node.clone().unwrap(); - let mount = currently_rendered.mount.get(); - let parent = { - let mounts = dom.runtime.mounts.borrow(); - mounts - .get(mount.0) - .expect("suspense placeholder is not mounted") - .parent - }; - - (currently_rendered, parent) + let Some(scope_state) = dom.scopes.get_mut(scope_id.0) else { + return; }; + // Reset the suspense context + let suspense_context = scope_state + .state() + .suspense_location() + .suspense_context() + .unwrap() + .clone(); + suspense_context.inner.suspended_tasks.borrow_mut().clear(); + + // Get the parent of the suspense boundary to later create children with the right parent + let currently_rendered = scope_state.last_rendered_node.clone().unwrap(); + let mount = currently_rendered.mount.get(); + let parent = { + let mounts = dom.runtime.mounts.borrow(); + mounts + .get(mount.0) + .expect("suspense placeholder is not mounted") + .parent + }; + + let props = Self::downcast_from_props(&mut *scope_state.props).unwrap(); + // Unmount any children to reset any scopes under this suspense boundary - let children = suspense_children(scope_id, dom); + let children = props.children.clone(); let suspense_context = SuspenseContext::downcast_suspense_boundary_from_scope(&dom.runtime, scope_id) .unwrap(); @@ -464,7 +465,7 @@ impl SuspenseBoundaryProps { } // Replace the rendered nodes with resolved nodes - currently_rendered.remove_node(&mut *dom, Some(&mut *to), Some(replace_with)); + currently_rendered.remove_node(&mut *dom, Some(to), Some(replace_with)); // Switch to only writing templates only_write_templates(to); @@ -473,141 +474,152 @@ impl SuspenseBoundaryProps { // First always render the children in the background. Rendering the children may cause this boundary to suspend suspense_context.under_suspense_boundary(&dom.runtime(), || { - children.create(dom, parent, Some(&mut *to)); + children.create(dom, parent, Some(to)); }); - // Store the (now mounted) children back - store_suspense_children(scope_id, dom, &children); - dom.scopes[scope_id.0].last_rendered_node = Some(children); + // Store the (now mounted) children back into the scope state + let scope_state = &mut dom.scopes[scope_id.0]; + let props = Self::downcast_from_props(&mut *scope_state.props).unwrap(); + props.children.clone_from(&children); + scope_state.last_rendered_node = Some(children); // Run any closures that were waiting for the suspense to resolve suspense_context.run_resolved_closures(&dom.runtime); }) } -} -/// Diff a suspense boundary scope against its current children/fallback props. -fn suspense_diff( - _driver: &SuspenseDriver, - scope_id: ScopeId, - dom: &mut VirtualDom, - to: Option<&mut (dyn WriteMutations + '_)>, -) { - dom.runtime.clone().with_scope_on_stack(scope_id, || { - let last_rendered_node = dom.scopes[scope_id.0].last_rendered_node.clone().unwrap(); - - let children = suspense_children(scope_id, dom); - let fallback = suspense_fallback(scope_id, dom); - - let suspense_context = dom.scopes[scope_id.0] - .state() - .suspense_boundary() - .unwrap() - .clone(); - let suspended_nodes = suspense_context.suspended_nodes(); - let suspended = !suspense_context.suspended_futures().is_empty(); - match (suspended_nodes, suspended) { - // We already have suspended nodes that still need to be suspended - // Just diff the normal and suspended nodes - (Some(suspended_nodes), true) => { - let new_suspended_nodes: VNode = children.as_vnode().clone(); - - // Diff the placeholder nodes in the dom - let new_placeholder = - suspense_context.in_suspense_placeholder(&dom.runtime(), || { - let new_placeholder = - LastRenderedNode::new(fallback.call(suspense_context.clone())); + pub(crate) fn diff( + scope_id: ScopeId, + dom: &mut VirtualDom, + to: Option<&mut (dyn WriteMutations + '_)>, + ) { + dom.runtime.clone().with_scope_on_stack(scope_id, || { + let scope = &mut dom.scopes[scope_id.0]; + let myself = Self::downcast_from_props(&mut *scope.props) + .unwrap() + .clone(); + + let last_rendered_node = scope.last_rendered_node.clone().unwrap(); + + let Self { + fallback, children, .. + } = myself; + + let suspense_context = scope.state().suspense_boundary().unwrap().clone(); + let suspended_nodes = suspense_context.suspended_nodes(); + let suspended = !suspense_context.suspended_futures().is_empty(); + match (suspended_nodes, suspended) { + // We already have suspended nodes that still need to be suspended + // Just diff the normal and suspended nodes + (Some(suspended_nodes), true) => { + let new_suspended_nodes: VNode = children.as_vnode().clone(); + + // Diff the placeholder nodes in the dom + let new_placeholder = + suspense_context.in_suspense_placeholder(&dom.runtime(), || { + let old_placeholder = last_rendered_node; + let new_placeholder = + LastRenderedNode::new(fallback.call(suspense_context.clone())); + + old_placeholder.diff_node(&new_placeholder, dom, to); + new_placeholder + }); + + // Set the last rendered node to the placeholder + dom.scopes[scope_id.0].last_rendered_node = Some(new_placeholder); + + // Diff the suspended nodes in the background + suspense_context.under_suspense_boundary(&dom.runtime(), || { + suspended_nodes.diff_node(&new_suspended_nodes, dom, None); + }); - last_rendered_node.diff_node(&new_placeholder, dom, to); - new_placeholder + let suspense_context = SuspenseContext::downcast_suspense_boundary_from_scope( + &dom.runtime, + scope_id, + ) + .unwrap(); + suspense_context.set_suspended_nodes(new_suspended_nodes); + } + // We have no suspended nodes, and we are not suspended. Just diff the children like normal + (None, false) => { + let old_children = last_rendered_node; + let new_children = children; + + suspense_context.under_suspense_boundary(&dom.runtime(), || { + old_children.diff_node(&new_children, dom, to); }); - // Set the last rendered node to the placeholder - dom.scopes[scope_id.0].last_rendered_node = Some(new_placeholder); + // Set the last rendered node to the new children + dom.scopes[scope_id.0].last_rendered_node = new_children.into(); + } + // We have no suspended nodes, but we just became suspended. Move the children to the background + (None, true) => { + let old_children = last_rendered_node; + let new_children: VNode = children.as_vnode().clone(); - // Diff the suspended nodes in the background - suspense_context.under_suspense_boundary(&dom.runtime(), || { - suspended_nodes.diff_node(&new_suspended_nodes, dom, None); - }); + let new_placeholder = + LastRenderedNode::new(fallback.call(suspense_context.clone())); - let suspense_context = - SuspenseContext::downcast_suspense_boundary_from_scope(&dom.runtime, scope_id) - .unwrap(); - suspense_context.set_suspended_nodes(new_suspended_nodes); + // Move the children to the background + let mount = old_children.mount.get(); + let parent = dom.get_mounted_parent(mount); - store_suspense_children(scope_id, dom, &children); - } - // We have no suspended nodes, and we are not suspended. Just diff the children like normal - (None, false) => { - suspense_context.under_suspense_boundary(&dom.runtime(), || { - last_rendered_node.diff_node(&children, dom, to); - }); - - // Set the last rendered node to the new children - store_suspense_children(scope_id, dom, &children); - dom.scopes[scope_id.0].last_rendered_node = Some(children); - } - // We have no suspended nodes, but we just became suspended. Move the children to the background - (None, true) => { - let old_children = last_rendered_node; - let new_children: VNode = children.as_vnode().clone(); - - let new_placeholder = - LastRenderedNode::new(fallback.call(suspense_context.clone())); - - // Move the children to the background - let mount = old_children.mount.get(); - let parent = dom.get_mounted_parent(mount); - - suspense_context.in_suspense_placeholder(&dom.runtime(), || { - old_children.move_node_to_background( - std::slice::from_ref(&new_placeholder), - parent, - dom, - to, - ); - }); - - // Then diff the new children in the background - suspense_context.under_suspense_boundary(&dom.runtime(), || { - old_children.diff_node(&new_children, dom, None); - }); - - // Set the last rendered node to the new suspense placeholder - dom.scopes[scope_id.0].last_rendered_node = Some(new_placeholder); + suspense_context.in_suspense_placeholder(&dom.runtime(), || { + old_children.move_node_to_background( + std::slice::from_ref(&new_placeholder), + parent, + dom, + to, + ); + }); - let suspense_context = - SuspenseContext::downcast_suspense_boundary_from_scope(&dom.runtime, scope_id) - .unwrap(); - suspense_context.set_suspended_nodes(new_children); + // Then diff the new children in the background + suspense_context.under_suspense_boundary(&dom.runtime(), || { + old_children.diff_node(&new_children, dom, None); + }); - store_suspense_children(scope_id, dom, &children); - un_resolve_suspense(dom, scope_id); - } - // We have suspended nodes, but we just got out of suspense. Move the suspended nodes to the foreground - (Some(_), false) => { - // Take the suspended nodes out of the suspense boundary so the children know that the boundary is not suspended while diffing - let old_suspended_nodes = suspense_context.take_suspended_nodes().unwrap(); - let old_placeholder = last_rendered_node; - - // First diff the two children nodes in the background - suspense_context.under_suspense_boundary(&dom.runtime(), || { - old_suspended_nodes.diff_node(&children, dom, None); - - // Then replace the placeholder with the new children - let mount = old_placeholder.mount.get(); - let parent = dom.get_mounted_parent(mount); - old_placeholder.replace(std::slice::from_ref(&children), parent, dom, to); - }); + // Set the last rendered node to the new suspense placeholder + dom.scopes[scope_id.0].last_rendered_node = Some(new_placeholder); - // Set the last rendered node to the new children - store_suspense_children(scope_id, dom, &children); - dom.scopes[scope_id.0].last_rendered_node = Some(children); + let suspense_context = SuspenseContext::downcast_suspense_boundary_from_scope( + &dom.runtime, + scope_id, + ) + .unwrap(); + suspense_context.set_suspended_nodes(new_children); + + un_resolve_suspense(dom, scope_id); + } + // We have suspended nodes, but we just got out of suspense. Move the suspended nodes to the foreground + (Some(_), false) => { + // Take the suspended nodes out of the suspense boundary so the children know that the boundary is not suspended while diffing + let old_suspended_nodes = suspense_context.take_suspended_nodes().unwrap(); + let old_placeholder = last_rendered_node; + let new_children = children; + + // First diff the two children nodes in the background + suspense_context.under_suspense_boundary(&dom.runtime(), || { + old_suspended_nodes.diff_node(&new_children, dom, None); + + // Then replace the placeholder with the new children + let mount = old_placeholder.mount.get(); + let parent = dom.get_mounted_parent(mount); + old_placeholder.replace( + std::slice::from_ref(&new_children), + parent, + dom, + to, + ); + }); - mark_suspense_resolved(&suspense_context, dom, scope_id); + // Set the last rendered node to the new children + dom.scopes[scope_id.0].last_rendered_node = Some(new_children); + + mark_suspense_resolved(&suspense_context, dom, scope_id); + } } - } - }) + }) + } } /// Move to a resolved suspense state