Improve warnings when trying to update outside of pending suspense boundaries on the server (#2575)

* Improve warnings when trying to update outside of pending suspense boundaries on the server

* remove suspense resolve log
This commit is contained in:
Evan Almloff authored and GitHub committed 2024-07-17 19:32:33 -07:00
1 parent 88af3e7eff
commit e794be5d78
14 files changed
+409 -313

No files matched your search

+1 -1
View File
@@ -83,7 +83,7 @@ impl<F: ComponentFunction<P, M> + Clone, P: Clone + 'static, M: 'static> AnyProp
Ok(node) => RenderReturn { node }, Ok(node) => RenderReturn { node },
Err(err) => { Err(err) => {
let component_name = self.name; let component_name = self.name;
tracing::error!("Error while rendering component `{component_name}`: {err:?}"); tracing::error!("Panic while rendering component `{component_name}`: {err:?}");
let panic = CapturedPanic { error: err }; let panic = CapturedPanic { error: err };
RenderReturn { RenderReturn {
node: Err(panic.into()), node: Err(panic.into()),
+3
View File
@@ -85,6 +85,9 @@ impl VirtualDom {
}; };
self.dirty_scopes.remove(&ScopeOrder::new(height, id)); self.dirty_scopes.remove(&ScopeOrder::new(height, id));
// If this scope was a suspense boundary, remove it from the resolved scopes
self.resolved_scopes.retain(|s| s != &id);
} }
} }
+35 -42
View File
@@ -10,6 +10,7 @@ use crate::{
VComponent, WriteMutations, VComponent, WriteMutations,
}, },
nodes::VNode, nodes::VNode,
prelude::SuspenseContext,
scopes::ScopeId, scopes::ScopeId,
virtual_dom::VirtualDom, virtual_dom::VirtualDom,
RenderReturn, RenderReturn,
@@ -22,7 +23,7 @@ impl VirtualDom {
scope_id: ScopeId, scope_id: ScopeId,
) { ) {
let scope = &mut self.scopes[scope_id.0]; let scope = &mut self.scopes[scope_id.0];
if SuspenseBoundaryProps::downcast_mut_from_props(&mut *scope.props).is_some() { if SuspenseBoundaryProps::downcast_from_props(&mut *scope.props).is_some() {
SuspenseBoundaryProps::diff(scope_id, self, to) SuspenseBoundaryProps::diff(scope_id, self, to)
} else { } else {
let new_nodes = self.run_scope(scope_id); let new_nodes = self.run_scope(scope_id);
@@ -37,29 +38,27 @@ impl VirtualDom {
scope: ScopeId, scope: ScopeId,
new_nodes: RenderReturn, new_nodes: RenderReturn,
) { ) {
// We don't diff the nodes if the scope is suspended or has an error self.runtime.clone().with_scope_on_stack(scope, || {
let Ok(new_real_nodes) = &new_nodes.node else { // We don't diff the nodes if the scope is suspended or has an error
return; let Ok(new_real_nodes) = &new_nodes.node else {
}; return;
};
let scope_state = &mut self.scopes[scope.0];
// Load the old and new rendered nodes
let old = scope_state.last_rendered_node.take().unwrap();
self.runtime.push_scope(scope); // If there are suspended scopes, we need to check if the scope is suspended before we diff it
let scope_state = &mut self.scopes[scope.0]; // If it is suspended, we need to diff it but write the mutations nothing
// Load the old and new rendered nodes // Note: It is important that we still diff the scope even if it is suspended, because the scope may render other child components which may change between renders
let old = scope_state.last_rendered_node.take().unwrap(); let mut render_to = to.filter(|_| self.runtime.scope_should_render(scope));
old.diff_node(new_real_nodes, self, render_to.as_deref_mut());
// If there are suspended scopes, we need to check if the scope is suspended before we diff it self.scopes[scope.0].last_rendered_node = Some(new_nodes);
// If it is suspended, we need to diff it but write the mutations nothing
// Note: It is important that we still diff the scope even if it is suspended, because the scope may render other child components which may change between renders
let mut render_to = to.filter(|_| self.runtime.scope_should_render(scope));
old.diff_node(new_real_nodes, self, render_to.as_deref_mut());
self.scopes[scope.0].last_rendered_node = Some(new_nodes); if render_to.is_some() {
self.runtime.get_state(scope).unwrap().mount(&self.runtime);
if render_to.is_some() { }
self.runtime.get_state(scope).unwrap().mount(&self.runtime); })
}
self.runtime.pop_scope();
} }
/// Create a new [`ScopeState`] for a component that has been created with [`VirtualDom::create_scope`] /// Create a new [`ScopeState`] for a component that has been created with [`VirtualDom::create_scope`]
@@ -73,25 +72,24 @@ impl VirtualDom {
new_nodes: RenderReturn, new_nodes: RenderReturn,
parent: Option<ElementRef>, parent: Option<ElementRef>,
) -> usize { ) -> usize {
self.runtime.push_scope(scope); self.runtime.clone().with_scope_on_stack(scope, || {
// If there are suspended scopes, we need to check if the scope is suspended before we diff it
// If it is suspended, we need to diff it but write the mutations nothing
// Note: It is important that we still diff the scope even if it is suspended, because the scope may render other child components which may change between renders
let mut render_to = to.filter(|_| self.runtime.scope_should_render(scope));
// If there are suspended scopes, we need to check if the scope is suspended before we diff it // Create the node
// If it is suspended, we need to diff it but write the mutations nothing let nodes = new_nodes.create(self, parent, render_to.as_deref_mut());
// Note: It is important that we still diff the scope even if it is suspended, because the scope may render other child components which may change between renders
let mut render_to = to.filter(|_| self.runtime.scope_should_render(scope));
// Create the node // Then set the new node as the last rendered node
let nodes = new_nodes.create(self, parent, render_to.as_deref_mut()); self.scopes[scope.0].last_rendered_node = Some(new_nodes);
// Then set the new node as the last rendered node if render_to.is_some() {
self.scopes[scope.0].last_rendered_node = Some(new_nodes); self.runtime.get_state(scope).unwrap().mount(&self.runtime);
}
if render_to.is_some() { nodes
self.runtime.get_state(scope).unwrap().mount(&self.runtime); })
}
self.runtime.pop_scope();
nodes
} }
pub(crate) fn remove_component_node<M: WriteMutations>( pub(crate) fn remove_component_node<M: WriteMutations>(
@@ -102,12 +100,7 @@ impl VirtualDom {
replace_with: Option<usize>, replace_with: Option<usize>,
) { ) {
// If this is a suspense boundary, remove the suspended nodes as well // If this is a suspense boundary, remove the suspended nodes as well
if let Some(mut suspense) = SuspenseContext::remove_suspended_nodes::<M>(self, scope_id, destroy_component_state);
SuspenseBoundaryProps::downcast_mut_from_props(&mut *self.scopes[scope_id.0].props)
.cloned()
{
suspense.remove_suspended_nodes::<M>(self, destroy_component_state);
}
// Remove the component from the dom // Remove the component from the dom
if let Some(node) = self.scopes[scope_id.0].last_rendered_node.as_ref() { if let Some(node) = self.scopes[scope_id.0].last_rendered_node.as_ref() {
+10 -7
View File
@@ -418,13 +418,16 @@ impl<Args: 'static, Ret: 'static> Callback<Args, Ret> {
/// This borrows the callback using a RefCell. Recursively calling a callback will cause a panic. /// This borrows the callback using a RefCell. Recursively calling a callback will cause a panic.
pub fn call(&self, arguments: Args) -> Ret { pub fn call(&self, arguments: Args) -> Ret {
if let Some(callback) = self.callback.read().as_ref() { if let Some(callback) = self.callback.read().as_ref() {
Runtime::with(|rt| rt.push_scope(self.origin)); Runtime::with(|rt| {
let value = { rt.with_scope_on_stack(self.origin, || {
let mut callback = callback.borrow_mut(); let value = {
callback(arguments) let mut callback = callback.borrow_mut();
}; callback(arguments)
Runtime::with(|rt| rt.pop_scope()); };
value value
})
})
.expect("Callback must be called from within the dioxus runtime")
} else { } else {
panic!("Callback was manually dropped") panic!("Callback was manually dropped")
} }
+33 -6
View File
@@ -22,10 +22,12 @@ pub struct Runtime {
pub(crate) scope_states: RefCell<Vec<Option<Scope>>>, pub(crate) scope_states: RefCell<Vec<Option<Scope>>>,
// We use this to track the current scope // We use this to track the current scope
pub(crate) scope_stack: RefCell<Vec<ScopeId>>, // This stack should only be modified through [`Runtime::with_scope_on_stack`] to ensure that the stack is correctly restored
scope_stack: RefCell<Vec<ScopeId>>,
// We use this to track the current suspense location. Generally this lines up with the scope stack, but it may be different for children of a suspense boundary // We use this to track the current suspense location. Generally this lines up with the scope stack, but it may be different for children of a suspense boundary
pub(crate) suspense_stack: RefCell<Vec<SuspenseLocation>>, // This stack should only be modified through [`Runtime::with_suspense_location`] to ensure that the stack is correctly restored
suspense_stack: RefCell<Vec<SuspenseLocation>>,
// We use this to track the current task // We use this to track the current task
pub(crate) current_task: Cell<Option<Task>>, pub(crate) current_task: Cell<Option<Task>>,
@@ -123,21 +125,46 @@ impl Runtime {
o o
} }
/// Get the current suspense location
pub(crate) fn current_suspense_location(&self) -> Option<SuspenseLocation> {
self.suspense_stack.borrow().last().cloned()
}
/// Run a callback a [`SuspenseLocation`] at the top of the stack
pub(crate) fn with_suspense_location<O>(
&self,
suspense_location: SuspenseLocation,
f: impl FnOnce() -> O,
) -> O {
self.suspense_stack.borrow_mut().push(suspense_location);
let o = f();
self.suspense_stack.borrow_mut().pop();
o
}
/// Run a callback with the current scope at the top of the stack
pub(crate) fn with_scope_on_stack<O>(&self, scope: ScopeId, f: impl FnOnce() -> O) -> O {
self.push_scope(scope);
let o = f();
self.pop_scope();
o
}
/// Push a scope onto the stack /// Push a scope onto the stack
pub(crate) fn push_scope(&self, scope: ScopeId) { fn push_scope(&self, scope: ScopeId) {
let suspense_location = self let suspense_location = self
.scope_states .scope_states
.borrow() .borrow()
.get(scope.0) .get(scope.0)
.and_then(|s| s.as_ref()) .and_then(|s| s.as_ref())
.map(|s| s.suspense_boundary()) .map(|s| s.suspense_location())
.unwrap_or_default(); .unwrap_or_default();
self.suspense_stack.borrow_mut().push(suspense_location); self.suspense_stack.borrow_mut().push(suspense_location);
self.scope_stack.borrow_mut().push(scope); self.scope_stack.borrow_mut().push(scope);
} }
/// Pop a scope off the stack /// Pop a scope off the stack
pub(crate) fn pop_scope(&self) { fn pop_scope(&self) {
self.scope_stack.borrow_mut().pop(); self.scope_stack.borrow_mut().pop();
self.suspense_stack.borrow_mut().pop(); self.suspense_stack.borrow_mut().pop();
} }
@@ -200,7 +227,7 @@ impl Runtime {
// If this is not a suspended scope, and we are under a frozen context, then we should // If this is not a suspended scope, and we are under a frozen context, then we should
let scopes = self.scope_states.borrow(); let scopes = self.scope_states.borrow();
let scope = &scopes[scope_id.0].as_ref().unwrap(); let scope = &scopes[scope_id.0].as_ref().unwrap();
!matches!(scope.suspense_boundary(), SuspenseLocation::UnderSuspense(suspense) if suspense.suspended()) !matches!(scope.suspense_location(), SuspenseLocation::UnderSuspense(suspense) if suspense.has_suspended_tasks())
} }
} }
+38 -40
View File
@@ -23,10 +23,7 @@ impl VirtualDom {
}; };
let suspense_boundary = self let suspense_boundary = self
.runtime .runtime
.suspense_stack .current_suspense_location()
.borrow()
.last()
.cloned()
.unwrap_or(SuspenseLocation::NotSuspended); .unwrap_or(SuspenseLocation::NotSuspended);
let entry = self.scopes.vacant_entry(); let entry = self.scopes.vacant_entry();
let id = ScopeId(entry.key()); let id = ScopeId(entry.key());
@@ -55,45 +52,46 @@ impl VirtualDom {
crate::Runtime::current().is_some(), crate::Runtime::current().is_some(),
"Must be in a dioxus runtime" "Must be in a dioxus runtime"
); );
self.runtime.push_scope(scope_id); self.runtime.clone().with_scope_on_stack(scope_id, || {
let scope = &self.scopes[scope_id.0];
let output = {
let scope_state = scope.state();
scope_state.hook_index.set(0);
// Run all pre-render hooks
for pre_run in scope_state.before_render.borrow_mut().iter_mut() {
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 mut render_return = props.render();
self.handle_element_return(
&mut render_return.node,
scope_id,
&scope.state(),
);
render_return
})
})
};
let scope = &self.scopes[scope_id.0];
let output = {
let scope_state = scope.state(); let scope_state = scope.state();
scope_state.hook_index.set(0); // Run all post-render hooks
for post_run in scope_state.after_render.borrow_mut().iter_mut() {
// Run all pre-render hooks post_run();
for pre_run in scope_state.before_render.borrow_mut().iter_mut() {
pre_run();
} }
let props: &dyn AnyProps = &*scope.props; // remove this scope from dirty scopes
self.dirty_scopes
let span = tracing::trace_span!("render", scope = %scope.state().name); .remove(&ScopeOrder::new(scope_state.height, scope_id));
span.in_scope(|| { output
scope.reactive_context.reset_and_run_in(|| { })
let mut render_return = props.render();
self.handle_element_return(&mut render_return.node, scope_id, &scope.state());
render_return
})
})
};
let scope_state = scope.state();
// 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));
self.runtime.pop_scope();
output
} }
/// Insert any errors, or suspended tasks from an element return into the runtime /// Insert any errors, or suspended tasks from an element return into the runtime
@@ -101,7 +99,7 @@ impl VirtualDom {
match node { match node {
Err(RenderError::Aborted(e)) => { Err(RenderError::Aborted(e)) => {
tracing::error!( tracing::error!(
"Error while rendering component `{}`: {e:?}", "Error while rendering component `{}`:\n{e}",
scope_state.name scope_state.name
); );
throw_error(e.clone_mounted()); throw_error(e.clone_mounted());
@@ -114,7 +112,7 @@ impl VirtualDom {
.runtime .runtime
.get_state(scope_id) .get_state(scope_id)
.unwrap() .unwrap()
.suspense_boundary(); .suspense_location();
let already_suspended = self let already_suspended = self
.runtime .runtime
.tasks .tasks
+18 -7
View File
@@ -24,8 +24,9 @@ pub(crate) enum ScopeStatus {
pub(crate) enum SuspenseLocation { pub(crate) enum SuspenseLocation {
#[default] #[default]
NotSuspended, NotSuspended,
InSuspensePlaceholder(SuspenseContext), SuspenseBoundary(SuspenseContext),
UnderSuspense(SuspenseContext), UnderSuspense(SuspenseContext),
InSuspensePlaceholder(SuspenseContext),
} }
impl SuspenseLocation { impl SuspenseLocation {
@@ -33,6 +34,7 @@ impl SuspenseLocation {
match self { match self {
SuspenseLocation::InSuspensePlaceholder(context) => Some(context), SuspenseLocation::InSuspensePlaceholder(context) => Some(context),
SuspenseLocation::UnderSuspense(context) => Some(context), SuspenseLocation::UnderSuspense(context) => Some(context),
SuspenseLocation::SuspenseBoundary(context) => Some(context),
_ => None, _ => None,
} }
} }
@@ -108,17 +110,26 @@ impl Scope {
} }
} }
/// Get the suspense boundary this scope is currently in (if any) /// Get the suspense location of this scope
pub(crate) fn suspense_boundary(&self) -> SuspenseLocation { pub(crate) fn suspense_location(&self) -> SuspenseLocation {
self.suspense_boundary.clone() self.suspense_boundary.clone()
} }
/// If this scope is a suspense boundary, return the suspense context
pub(crate) fn suspense_boundary(&self) -> Option<SuspenseContext> {
match self.suspense_location() {
SuspenseLocation::SuspenseBoundary(context) => Some(context),
_ => None,
}
}
/// Check if a node should run during suspense /// Check if a node should run during suspense
pub(crate) fn should_run_during_suspense(&self) -> bool { pub(crate) fn should_run_during_suspense(&self) -> bool {
matches!( let Some(context) = self.suspense_boundary.suspense_context() else {
self.suspense_boundary, return false;
SuspenseLocation::UnderSuspense(_) | SuspenseLocation::InSuspensePlaceholder(_) };
)
!context.frozen()
} }
/// Mark this scope as dirty, and schedule a render for it. /// Mark this scope as dirty, and schedule a render for it.
+155 -139
View File
@@ -1,4 +1,4 @@
use crate::innerlude::*; use crate::{innerlude::*, scope_context::SuspenseLocation};
/// Properties for the [`SuspenseBoundary()`] component. /// Properties for the [`SuspenseBoundary()`] component.
#[allow(non_camel_case_types)] #[allow(non_camel_case_types)]
@@ -6,8 +6,6 @@ pub struct SuspenseBoundaryProps {
fallback: Callback<SuspenseContext, Element>, fallback: Callback<SuspenseContext, Element>,
/// The children of the suspense boundary /// The children of the suspense boundary
children: Element, children: Element,
/// THe nodes that are suspended under this boundary
pub suspended_nodes: Option<VNode>,
} }
impl Clone for SuspenseBoundaryProps { impl Clone for SuspenseBoundaryProps {
@@ -15,10 +13,6 @@ impl Clone for SuspenseBoundaryProps {
Self { Self {
fallback: self.fallback, fallback: self.fallback,
children: self.children.clone(), children: self.children.clone(),
suspended_nodes: self
.suspended_nodes
.as_ref()
.map(|node| node.clone_mounted()),
} }
} }
} }
@@ -211,11 +205,7 @@ impl<__children: SuspenseBoundaryPropsBuilder_Optional<Element>>
let fallback = fallback.0; let fallback = fallback.0;
let children = SuspenseBoundaryPropsBuilder_Optional::into_value(children, VNode::empty); let children = SuspenseBoundaryPropsBuilder_Optional::into_value(children, VNode::empty);
SuspenseBoundaryPropsWithOwner { SuspenseBoundaryPropsWithOwner {
inner: SuspenseBoundaryProps { inner: SuspenseBoundaryProps { fallback, children },
fallback,
children,
suspended_nodes: None,
},
owner: self.owner, owner: self.owner,
} }
} }
@@ -272,29 +262,11 @@ pub use SuspenseBoundary_completions::Component::SuspenseBoundary;
/// Suspense has a custom diffing algorithm that diffs the suspended nodes in the background without rendering them /// Suspense has a custom diffing algorithm that diffs the suspended nodes in the background without rendering them
impl SuspenseBoundaryProps { impl SuspenseBoundaryProps {
/// Try to downcast [`AnyProps`] to [`SuspenseBoundaryProps`] /// Try to downcast [`AnyProps`] to [`SuspenseBoundaryProps`]
pub(crate) fn downcast_mut_from_props(props: &mut dyn AnyProps) -> Option<&mut Self> { pub(crate) fn downcast_from_props(props: &mut dyn AnyProps) -> Option<&mut Self> {
let inner: Option<&mut SuspenseBoundaryPropsWithOwner> = props.props_mut().downcast_mut(); let inner: Option<&mut SuspenseBoundaryPropsWithOwner> = props.props_mut().downcast_mut();
inner.map(|inner| &mut inner.inner) inner.map(|inner| &mut inner.inner)
} }
/// Try to downcast [`AnyProps`] to [`SuspenseBoundaryProps`]
pub(crate) fn downcast_ref_from_props(props: &dyn AnyProps) -> Option<&Self> {
let inner: Option<&SuspenseBoundaryPropsWithOwner> = props.props().downcast_ref();
inner.map(|inner| &inner.inner)
}
/// Try to extract [`SuspenseBoundaryProps`] from [`ScopeState`]
pub fn downcast_from_scope(scope_state: &ScopeState) -> Option<&Self> {
let inner: Option<&SuspenseBoundaryPropsWithOwner> =
scope_state.props.props().downcast_ref();
inner.map(|inner| &inner.inner)
}
/// Check if the suspense boundary is currently holding its children in suspense
pub fn suspended(&self) -> bool {
self.suspended_nodes.is_some()
}
pub(crate) fn create<M: WriteMutations>( pub(crate) fn create<M: WriteMutations>(
mount: MountId, mount: MountId,
idx: usize, idx: usize,
@@ -310,17 +282,19 @@ impl SuspenseBoundaryProps {
{ {
let suspense_context = SuspenseContext::new(); let suspense_context = SuspenseContext::new();
dom.runtime.suspense_stack.borrow_mut().push( let suspense_boundary_location =
crate::scope_context::SuspenseLocation::UnderSuspense(suspense_context.clone()), crate::scope_context::SuspenseLocation::SuspenseBoundary(
); suspense_context.clone(),
{ );
let scope_state = dom dom.runtime
.new_scope(component.props.duplicate(), component.name) .clone()
.state(); .with_suspense_location(suspense_boundary_location, || {
suspense_context.mount(scope_state.id); let scope_state = dom
scope_id = scope_state.id; .new_scope(component.props.duplicate(), component.name)
} .state();
dom.runtime.suspense_stack.borrow_mut().pop(); suspense_context.mount(scope_state.id);
scope_id = scope_state.id;
});
} }
// Store the scope id for the next render // Store the scope id for the next render
@@ -328,7 +302,9 @@ impl SuspenseBoundaryProps {
} }
let scope_state = &mut dom.scopes[scope_id.0]; let scope_state = &mut dom.scopes[scope_id.0];
let props = Self::downcast_mut_from_props(&mut *scope_state.props).unwrap(); 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 = RenderReturn { let children = RenderReturn {
node: props node: props
@@ -339,38 +315,41 @@ impl SuspenseBoundaryProps {
}; };
// First always render the children in the background. Rendering the children may cause this boundary to suspend // First always render the children in the background. Rendering the children may cause this boundary to suspend
dom.runtime.push_scope(scope_id); suspense_context.under_suspense_boundary(&dom.runtime(), || {
children.create(dom, parent, None::<&mut M>); children.create(dom, parent, None::<&mut M>);
dom.runtime.pop_scope(); });
// Store the (now mounted) children back into the scope state // Store the (now mounted) children back into the scope state
let scope_state = &mut dom.scopes[scope_id.0]; let scope_state = &mut dom.scopes[scope_id.0];
let props = Self::downcast_mut_from_props(&mut *scope_state.props).unwrap(); let props = Self::downcast_from_props(&mut *scope_state.props).unwrap();
props.children = children.clone().node; props.children = children.clone().node;
let scope_state = &mut dom.scopes[scope_id.0]; let scope_state = &mut dom.scopes[scope_id.0];
let suspense_context = scope_state let suspense_context = scope_state
.state() .state()
.suspense_boundary() .suspense_location()
.suspense_context() .suspense_context()
.unwrap() .unwrap()
.clone(); .clone();
// If there are suspended futures, render the fallback // If there are suspended futures, render the fallback
let nodes_created = if !suspense_context.suspended_futures().is_empty() { let nodes_created = if !suspense_context.suspended_futures().is_empty() {
let props = Self::downcast_mut_from_props(&mut *scope_state.props).unwrap(); let (node, nodes_created) =
props.suspended_nodes = Some(children.into()); suspense_context.in_suspense_placeholder(&dom.runtime(), || {
let scope_state = &mut dom.scopes[scope_id.0];
dom.runtime.suspense_stack.borrow_mut().push( let props = Self::downcast_from_props(&mut *scope_state.props).unwrap();
crate::scope_context::SuspenseLocation::InSuspensePlaceholder( let suspense_context = SuspenseContext::downcast_suspense_boundary_from_scope(
suspense_context.clone(), &dom.runtime,
), scope_id,
); )
let suspense_placeholder = props.fallback.call(suspense_context); .unwrap();
let node = RenderReturn { suspense_context.set_suspended_nodes(children.into());
node: suspense_placeholder, let suspense_placeholder = props.fallback.call(suspense_context);
}; let node = RenderReturn {
let nodes_created = node.create(dom, parent, to); node: suspense_placeholder,
dom.runtime.suspense_stack.borrow_mut().pop(); };
let nodes_created = node.create(dom, parent, to);
(node, nodes_created)
});
let scope_state = &mut dom.scopes[scope_id.0]; let scope_state = &mut dom.scopes[scope_id.0];
scope_state.last_rendered_node = Some(node); scope_state.last_rendered_node = Some(node);
@@ -378,14 +357,17 @@ impl SuspenseBoundaryProps {
nodes_created nodes_created
} else { } else {
// Otherwise just render the children in the real dom // Otherwise just render the children in the real dom
dom.runtime.push_scope(scope_id);
debug_assert!(children.mount.get().mounted()); debug_assert!(children.mount.get().mounted());
let nodes_created = children.create(dom, parent, to); let nodes_created = suspense_context
dom.runtime.pop_scope(); .under_suspense_boundary(&dom.runtime(), || children.create(dom, parent, to));
let scope_state = &mut dom.scopes[scope_id.0]; let scope_state = &mut dom.scopes[scope_id.0];
scope_state.last_rendered_node = Some(children); scope_state.last_rendered_node = Some(children);
let props = Self::downcast_mut_from_props(&mut *dom.scopes[scope_id.0].props).unwrap(); let suspense_context =
props.suspended_nodes = None; SuspenseContext::downcast_suspense_boundary_from_scope(&dom.runtime, scope_id)
.unwrap();
suspense_context.take_suspended_nodes();
mark_suspense_resolved(dom, scope_id);
nodes_created nodes_created
}; };
@@ -403,7 +385,7 @@ impl SuspenseBoundaryProps {
only_write_templates: impl FnOnce(&mut M), only_write_templates: impl FnOnce(&mut M),
replace_with: usize, replace_with: usize,
) { ) {
let _runtime = RuntimeGuard::new(dom.runtime.clone()); let _runtime = RuntimeGuard::new(dom.runtime());
let Some(scope_state) = dom.scopes.get_mut(scope_id.0) else { let Some(scope_state) = dom.scopes.get_mut(scope_id.0) else {
return; return;
}; };
@@ -411,7 +393,7 @@ impl SuspenseBoundaryProps {
// Reset the suspense context // Reset the suspense context
let suspense_context = scope_state let suspense_context = scope_state
.state() .state()
.suspense_boundary() .suspense_location()
.suspense_context() .suspense_context()
.unwrap() .unwrap()
.clone(); .clone();
@@ -426,7 +408,7 @@ impl SuspenseBoundaryProps {
.expect("suspense placeholder is not mounted") .expect("suspense placeholder is not mounted")
.parent; .parent;
let props = Self::downcast_mut_from_props(&mut *scope_state.props).unwrap(); let props = Self::downcast_from_props(&mut *scope_state.props).unwrap();
// Unmount any children to reset any scopes under this suspense boundary // Unmount any children to reset any scopes under this suspense boundary
let children = props let children = props
@@ -434,10 +416,9 @@ impl SuspenseBoundaryProps {
.as_ref() .as_ref()
.map(|node| node.clone_mounted()) .map(|node| node.clone_mounted())
.map_err(Clone::clone); .map_err(Clone::clone);
let suspended = props let suspense_context =
.suspended_nodes SuspenseContext::downcast_suspense_boundary_from_scope(&dom.runtime, scope_id).unwrap();
.as_ref() let suspended = suspense_context.suspended_nodes();
.map(|node| node.clone_mounted());
if let Some(node) = suspended { if let Some(node) = suspended {
node.remove_node(&mut *dom, None::<&mut M>, None); node.remove_node(&mut *dom, None::<&mut M>, None);
} }
@@ -451,16 +432,18 @@ impl SuspenseBoundaryProps {
children.mount.take(); children.mount.take();
// First always render the children in the background. Rendering the children may cause this boundary to suspend // First always render the children in the background. Rendering the children may cause this boundary to suspend
dom.runtime.push_scope(scope_id); suspense_context.under_suspense_boundary(&dom.runtime(), || {
children.create(dom, parent, Some(to)); children.create(dom, parent, Some(to));
dom.runtime.pop_scope(); });
// Store the (now mounted) children back into the scope state // Store the (now mounted) children back into the scope state
let scope_state = &mut dom.scopes[scope_id.0]; let scope_state = &mut dom.scopes[scope_id.0];
let props = Self::downcast_mut_from_props(&mut *scope_state.props).unwrap(); let props = Self::downcast_from_props(&mut *scope_state.props).unwrap();
props.children = children.clone().node; props.children = children.clone().node;
scope_state.last_rendered_node = Some(children); scope_state.last_rendered_node = Some(children);
props.suspended_nodes = None; let suspense_context =
SuspenseContext::downcast_suspense_boundary_from_scope(&dom.runtime, scope_id).unwrap();
suspense_context.take_suspended_nodes();
} }
pub(crate) fn diff<M: WriteMutations>( pub(crate) fn diff<M: WriteMutations>(
@@ -469,25 +452,18 @@ impl SuspenseBoundaryProps {
to: Option<&mut M>, to: Option<&mut M>,
) { ) {
let scope = &mut dom.scopes[scope_id.0]; let scope = &mut dom.scopes[scope_id.0];
let myself = Self::downcast_mut_from_props(&mut *scope.props) let myself = Self::downcast_from_props(&mut *scope.props)
.unwrap() .unwrap()
.clone(); .clone();
let last_rendered_node = scope.last_rendered_node.as_ref().unwrap().clone_mounted(); let last_rendered_node = scope.last_rendered_node.as_ref().unwrap().clone_mounted();
let Self { let Self {
fallback, fallback, children, ..
children,
suspended_nodes,
..
} = myself; } = myself;
let suspense_context = scope let suspense_context = scope.state().suspense_boundary().unwrap().clone();
.state() let suspended_nodes = suspense_context.suspended_nodes();
.suspense_boundary()
.suspense_context()
.unwrap()
.clone();
let suspended = !suspense_context.suspended_futures().is_empty(); let suspended = !suspense_context.suspended_futures().is_empty();
match (suspended_nodes, suspended) { match (suspended_nodes, suspended) {
// We already have suspended nodes that still need to be suspended // We already have suspended nodes that still need to be suspended
@@ -496,39 +472,38 @@ impl SuspenseBoundaryProps {
let new_suspended_nodes: VNode = RenderReturn { node: children }.into(); let new_suspended_nodes: VNode = RenderReturn { node: children }.into();
// Diff the placeholder nodes in the dom // Diff the placeholder nodes in the dom
dom.runtime.suspense_stack.borrow_mut().push( let new_placeholder =
crate::scope_context::SuspenseLocation::InSuspensePlaceholder( suspense_context.in_suspense_placeholder(&dom.runtime(), || {
suspense_context.clone(), let old_placeholder = last_rendered_node;
), let new_placeholder = RenderReturn {
); node: fallback.call(suspense_context.clone()),
let old_placeholder = last_rendered_node; };
let new_placeholder = RenderReturn {
node: fallback.call(suspense_context),
};
old_placeholder.diff_node(&new_placeholder, dom, to); old_placeholder.diff_node(&new_placeholder, dom, to);
dom.runtime.suspense_stack.borrow_mut().pop(); new_placeholder
});
// Set the last rendered node to the placeholder // Set the last rendered node to the placeholder
dom.scopes[scope_id.0].last_rendered_node = Some(new_placeholder); dom.scopes[scope_id.0].last_rendered_node = Some(new_placeholder);
// Diff the suspended nodes in the background // Diff the suspended nodes in the background
dom.runtime.push_scope(scope_id); suspense_context.under_suspense_boundary(&dom.runtime(), || {
suspended_nodes.diff_node(&new_suspended_nodes, dom, None::<&mut M>); suspended_nodes.diff_node(&new_suspended_nodes, dom, None::<&mut M>);
dom.runtime.pop_scope(); });
let props = let suspense_context =
Self::downcast_mut_from_props(&mut *dom.scopes[scope_id.0].props).unwrap(); SuspenseContext::downcast_suspense_boundary_from_scope(&dom.runtime, scope_id)
props.suspended_nodes = Some(new_suspended_nodes); .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 // We have no suspended nodes, and we are not suspended. Just diff the children like normal
(None, false) => { (None, false) => {
let old_children = last_rendered_node; let old_children = last_rendered_node;
let new_children = RenderReturn { node: children }; let new_children = RenderReturn { node: children };
dom.runtime.push_scope(scope_id); suspense_context.under_suspense_boundary(&dom.runtime(), || {
old_children.diff_node(&new_children, dom, to); old_children.diff_node(&new_children, dom, to);
dom.runtime.pop_scope(); });
// Set the last rendered node to the new children // Set the last rendered node to the new children
dom.scopes[scope_id.0].last_rendered_node = Some(new_children); dom.scopes[scope_id.0].last_rendered_node = Some(new_children);
@@ -546,61 +521,102 @@ impl SuspenseBoundaryProps {
let mount = old_children.mount.get(); let mount = old_children.mount.get();
let mount = dom.mounts.get(mount.0).expect("mount should exist"); let mount = dom.mounts.get(mount.0).expect("mount should exist");
let parent = mount.parent; let parent = mount.parent;
dom.runtime.push_scope(scope_id);
dom.runtime.suspense_stack.borrow_mut().push( suspense_context.in_suspense_placeholder(&dom.runtime(), || {
crate::scope_context::SuspenseLocation::InSuspensePlaceholder(suspense_context), old_children.move_node_to_background(
); std::slice::from_ref(&*new_placeholder),
old_children.move_node_to_background( parent,
std::slice::from_ref(&*new_placeholder), dom,
parent, to,
dom, );
to, });
);
dom.runtime.suspense_stack.borrow_mut().pop();
// Then diff the new children in the background // Then diff the new children in the background
old_children.diff_node(&new_children, dom, None::<&mut M>); suspense_context.under_suspense_boundary(&dom.runtime(), || {
dom.runtime.pop_scope(); old_children.diff_node(&new_children, dom, None::<&mut M>);
});
// Set the last rendered node to the new suspense placeholder // Set the last rendered node to the new suspense placeholder
dom.scopes[scope_id.0].last_rendered_node = Some(new_placeholder); dom.scopes[scope_id.0].last_rendered_node = Some(new_placeholder);
let props = let suspense_context =
Self::downcast_mut_from_props(&mut *dom.scopes[scope_id.0].props).unwrap(); SuspenseContext::downcast_suspense_boundary_from_scope(&dom.runtime, scope_id)
props.suspended_nodes = Some(new_children); .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 } // We have suspended nodes, but we just got out of suspense. Move the suspended nodes to the foreground
(Some(old_suspended_nodes), false) => { (Some(old_suspended_nodes), false) => {
let old_placeholder = last_rendered_node; let old_placeholder = last_rendered_node;
let new_children = RenderReturn { node: children }; let new_children = RenderReturn { node: children };
// First diff the two children nodes in the background // First diff the two children nodes in the background
dom.runtime.push_scope(scope_id); suspense_context.under_suspense_boundary(&dom.runtime(), || {
old_suspended_nodes.diff_node(&new_children, dom, None::<&mut M>); old_suspended_nodes.diff_node(&new_children, dom, None::<&mut M>);
// Then replace the placeholder with the new children // Then replace the placeholder with the new children
let mount = old_placeholder.mount.get(); let mount = old_placeholder.mount.get();
let mount = dom.mounts.get(mount.0).expect("mount should exist"); let mount = dom.mounts.get(mount.0).expect("mount should exist");
let parent = mount.parent; let parent = mount.parent;
old_placeholder.replace(std::slice::from_ref(&*new_children), parent, dom, to); old_placeholder.replace(std::slice::from_ref(&*new_children), parent, dom, to);
dom.runtime.pop_scope(); });
// Set the last rendered node to the new children // Set the last rendered node to the new children
dom.scopes[scope_id.0].last_rendered_node = Some(new_children); dom.scopes[scope_id.0].last_rendered_node = Some(new_children);
let props = let suspense_context =
Self::downcast_mut_from_props(&mut *dom.scopes[scope_id.0].props).unwrap(); SuspenseContext::downcast_suspense_boundary_from_scope(&dom.runtime, scope_id)
props.suspended_nodes = None; .unwrap();
suspense_context.take_suspended_nodes();
mark_suspense_resolved(dom, scope_id);
} }
} }
} }
}
/// Move to a resolved suspense state
fn mark_suspense_resolved(dom: &mut VirtualDom, scope_id: ScopeId) {
dom.resolved_scopes.push(scope_id);
}
/// Move from a resolved suspense state to an suspended state
fn un_resolve_suspense(dom: &mut VirtualDom, scope_id: ScopeId) {
dom.resolved_scopes.retain(|&id| id != scope_id);
}
impl SuspenseContext {
/// Run a closure under a suspense boundary
pub fn under_suspense_boundary<O>(&self, runtime: &Runtime, f: impl FnOnce() -> O) -> O {
runtime.with_suspense_location(SuspenseLocation::UnderSuspense(self.clone()), f)
}
/// Run a closure under a suspense placeholder
pub fn in_suspense_placeholder<O>(&self, runtime: &Runtime, f: impl FnOnce() -> O) -> O {
runtime.with_suspense_location(SuspenseLocation::InSuspensePlaceholder(self.clone()), f)
}
/// Try to get a suspense boundary from a scope id
pub fn downcast_suspense_boundary_from_scope(
runtime: &Runtime,
scope_id: ScopeId,
) -> Option<Self> {
runtime
.get_state(scope_id)
.and_then(|scope| scope.suspense_boundary())
}
pub(crate) fn remove_suspended_nodes<M: WriteMutations>( pub(crate) fn remove_suspended_nodes<M: WriteMutations>(
&mut self,
dom: &mut VirtualDom, dom: &mut VirtualDom,
scope_id: ScopeId,
destroy_component_state: bool, destroy_component_state: bool,
) { ) {
let Some(scope) = Self::downcast_suspense_boundary_from_scope(&dom.runtime, scope_id)
else {
return;
};
// Remove the suspended nodes // Remove the suspended nodes
if let Some(node) = self.suspended_nodes.take() { 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, None::<&mut M>, destroy_component_state, None)
} }
} }
+39 -1
View File
@@ -100,6 +100,8 @@ impl SuspenseContext {
inner: Rc::new(SuspenseBoundaryInner { inner: Rc::new(SuspenseBoundaryInner {
suspended_tasks: RefCell::new(vec![]), suspended_tasks: RefCell::new(vec![]),
id: Cell::new(ScopeId::ROOT), id: Cell::new(ScopeId::ROOT),
suspended_nodes: Default::default(),
frozen: Default::default(),
}), }),
} }
} }
@@ -109,8 +111,40 @@ impl SuspenseContext {
self.inner.id.set(scope); self.inner.id.set(scope);
} }
/// Get the suspense boundary's suspended nodes
pub fn suspended_nodes(&self) -> Option<VNode> {
self.inner
.suspended_nodes
.borrow()
.as_ref()
.map(|node| node.clone_mounted())
}
/// Set the suspense boundary's suspended nodes
pub(crate) fn set_suspended_nodes(&self, suspended_nodes: VNode) {
self.inner
.suspended_nodes
.borrow_mut()
.replace(suspended_nodes);
}
/// Take the suspense boundary's suspended nodes
pub(crate) fn take_suspended_nodes(&self) -> Option<VNode> {
self.inner.suspended_nodes.borrow_mut().take()
}
/// Check if the suspense boundary is resolved and frozen
pub fn frozen(&self) -> bool {
self.inner.frozen.get()
}
/// Resolve the suspense boundary on the server and freeze it to prevent future reruns of any child nodes of the suspense boundary
pub fn freeze(&self) {
self.inner.frozen.set(true);
}
/// Check if there are any suspended tasks /// Check if there are any suspended tasks
pub fn suspended(&self) -> bool { pub fn has_suspended_tasks(&self) -> bool {
!self.inner.suspended_tasks.borrow().is_empty() !self.inner.suspended_tasks.borrow().is_empty()
} }
@@ -152,6 +186,10 @@ impl SuspenseContext {
pub struct SuspenseBoundaryInner { pub struct SuspenseBoundaryInner {
suspended_tasks: RefCell<Vec<SuspendedFuture>>, suspended_tasks: RefCell<Vec<SuspendedFuture>>,
id: Cell<ScopeId>, id: Cell<ScopeId>,
/// The nodes that are suspended under this boundary
suspended_nodes: RefCell<Option<VNode>>,
/// On the server, you can only resolve a suspense boundary once. This is used to track if the suspense boundary has been resolved and if it should be frozen
frozen: Cell<bool>,
} }
/// Provides context methods to [`Result<T, RenderError>`] to show loading indicators for suspended results /// Provides context methods to [`Result<T, RenderError>`] to show loading indicators for suspended results
+16 -16
View File
@@ -262,26 +262,26 @@ impl Runtime {
let mut cx = std::task::Context::from_waker(&task.waker); let mut cx = std::task::Context::from_waker(&task.waker);
// update the scope stack // poll the future with the scope on the stack
self.push_scope(task.scope); let poll_result = self.with_scope_on_stack(task.scope, || {
self.rendering.set(false); self.rendering.set(false);
self.current_task.set(Some(id)); self.current_task.set(Some(id));
let poll_result = task.task.borrow_mut().as_mut().poll(&mut cx); let poll_result = task.task.borrow_mut().as_mut().poll(&mut cx);
if poll_result.is_ready() { if poll_result.is_ready() {
// Remove it from the scope so we dont try to double drop it when the scope dropes // Remove it from the scope so we dont try to double drop it when the scope dropes
self.get_state(task.scope) self.get_state(task.scope)
.unwrap() .unwrap()
.spawned_tasks .spawned_tasks
.borrow_mut() .borrow_mut()
.remove(&id); .remove(&id);
self.remove_task(id); self.remove_task(id);
} }
// Remove the scope from the stack poll_result
self.pop_scope(); });
self.rendering.set(true); self.rendering.set(true);
self.current_task.set(None); self.current_task.set(None);
+15 -25
View File
@@ -2,7 +2,7 @@
//! //!
//! This module provides the primary mechanics to create a hook-based, concurrent VDOM for Rust. //! This module provides the primary mechanics to create a hook-based, concurrent VDOM for Rust.
use crate::innerlude::{SuspenseBoundaryProps, Work}; use crate::innerlude::Work;
use crate::properties::RootProps; use crate::properties::RootProps;
use crate::root_wrapper::RootScopeWrapper; use crate::root_wrapper::RootScopeWrapper;
use crate::{ use crate::{
@@ -225,6 +225,9 @@ pub struct VirtualDom {
pub(crate) runtime: Rc<Runtime>, pub(crate) runtime: Rc<Runtime>,
// The scopes that have been resolved since the last render
pub(crate) resolved_scopes: Vec<ScopeId>,
rx: futures_channel::mpsc::UnboundedReceiver<SchedulerMsg>, rx: futures_channel::mpsc::UnboundedReceiver<SchedulerMsg>,
} }
@@ -330,6 +333,7 @@ impl VirtualDom {
queued_templates: Default::default(), queued_templates: Default::default(),
elements: Default::default(), elements: Default::default(),
mounts: Default::default(), mounts: Default::default(),
resolved_scopes: Default::default(),
}; };
let root = VProps::new( let root = VProps::new(
@@ -776,8 +780,6 @@ impl VirtualDom {
// Queue any new events before we start working // Queue any new events before we start working
self.queue_events(); self.queue_events();
let mut resolved_scopes = Vec::new();
// Render whatever work needs to be rendered, unlocking new futures and suspense leaves // Render whatever work needs to be rendered, unlocking new futures and suspense leaves
let _runtime = RuntimeGuard::new(self.runtime.clone()); let _runtime = RuntimeGuard::new(self.runtime.clone());
@@ -791,34 +793,21 @@ impl VirtualDom {
} }
} }
Work::RerunScope(scope) => { Work::RerunScope(scope) => {
if self let scope_id: ScopeId = scope.id;
let run_scope = self
.runtime .runtime
.get_state(scope.id) .get_state(scope.id)
.filter(|scope| scope.should_run_during_suspense()) .filter(|scope| scope.should_run_during_suspense())
.is_some() .is_some();
{ if run_scope {
let scope_state = self.get_scope(scope.id).unwrap();
let was_suspended =
SuspenseBoundaryProps::downcast_ref_from_props(&*scope_state.props)
.filter(|props| props.suspended())
.is_some();
// If the scope is dirty, run the scope and get the mutations // If the scope is dirty, run the scope and get the mutations
self.run_and_diff_scope(None::<&mut NoOpMutations>, scope.id); self.run_and_diff_scope(None::<&mut NoOpMutations>, scope_id);
let scope_state = self.get_scope(scope.id).unwrap();
let is_now_suspended =
SuspenseBoundaryProps::downcast_ref_from_props(&*scope_state.props)
.filter(|props| props.suspended())
.is_some();
if is_now_suspended { tracing::trace!("Ran scope {:?} during suspense", scope_id);
resolved_scopes.retain(|&id| id != scope.id);
} else if was_suspended {
resolved_scopes.push(scope.id);
}
} else { } else {
tracing::warn!( tracing::warn!(
"Scope {:?} was marked as dirty, but will not rerun during suspense. Only nodes that are under a suspense boundary rerun during suspense", "Scope {:?} was marked as dirty, but will not rerun during suspense. Only nodes that are under a suspense boundary rerun during suspense",
scope.id scope_id
); );
} }
} }
@@ -833,8 +822,9 @@ impl VirtualDom {
} }
} }
resolved_scopes.sort_by_key(|&id| self.runtime.get_state(id).unwrap().height); self.resolved_scopes
resolved_scopes .sort_by_key(|&id| self.runtime.get_state(id).unwrap().height);
std::mem::take(&mut self.resolved_scopes)
} }
/// Get the current runtime /// Get the current runtime
@@ -1,6 +1,7 @@
use dioxus_lib::prelude::dioxus_core::DynamicNode; use dioxus_lib::prelude::dioxus_core::DynamicNode;
use dioxus_lib::prelude::{ use dioxus_lib::prelude::{
has_context, try_consume_context, ScopeId, SuspenseBoundaryProps, VNode, VirtualDom, has_context, try_consume_context, ScopeId, SuspenseBoundaryProps, SuspenseContext, VNode,
VirtualDom,
}; };
use serde::Serialize; use serde::Serialize;
@@ -49,9 +50,11 @@ impl super::HTMLData {
// then continue to any children // then continue to any children
if let Some(scope) = vdom.get_scope(scope) { if let Some(scope) = vdom.get_scope(scope) {
// If this is a suspense boundary, move into the children first (even if they are suspended) because that will be run first on the client // If this is a suspense boundary, move into the children first (even if they are suspended) because that will be run first on the client
if let Some(suspense_boundary) = SuspenseBoundaryProps::downcast_from_scope(scope) { if let Some(suspense_boundary) =
if let Some(node) = suspense_boundary.suspended_nodes.as_ref() { SuspenseContext::downcast_suspense_boundary_from_scope(&vdom.runtime(), scope.id())
self.take_from_vnode(vdom, node); {
if let Some(node) = suspense_boundary.suspended_nodes() {
self.take_from_vnode(vdom, &node);
} }
} }
if let Some(node) = scope.try_root_node() { if let Some(node) = scope.try_root_node() {
+35 -23
View File
@@ -207,10 +207,12 @@ impl SsrRendererPool {
let scope_to_mount_mapping = scope_to_mount_mapping.clone(); let scope_to_mount_mapping = scope_to_mount_mapping.clone();
let stream = stream.clone(); let stream = stream.clone();
renderer.set_render_components(move |renderer, to, vdom, scope| { renderer.set_render_components(move |renderer, to, vdom, scope| {
let is_suspense_boundary = vdom let is_suspense_boundary =
.get_scope(scope) SuspenseContext::downcast_suspense_boundary_from_scope(
.and_then(|s| SuspenseBoundaryProps::downcast_from_scope(s)) &vdom.runtime(),
.filter(|s| s.suspended()) scope,
)
.filter(|s| s.has_suspended_tasks())
.is_some(); .is_some();
if is_suspense_boundary { if is_suspense_boundary {
let mount = stream.render_placeholder( let mount = stream.render_placeholder(
@@ -265,27 +267,37 @@ impl SsrRendererPool {
for scope in resolved_suspense_nodes { for scope in resolved_suspense_nodes {
let mount = { let mount = {
let mut lock = scope_to_mount_mapping.write().unwrap(); let mut lock = scope_to_mount_mapping.write().unwrap();
lock.remove(&scope).unwrap() lock.remove(&scope)
}; };
let mut resolved_chunk = String::new(); // If the suspense boundary was immediately removed, it may not have a mount. We can just skip resolving it
// After we replace the placeholder in the dom with javascript, we need to send down the resolved data so that the client can hydrate the node if let Some(mount) = mount {
let render_suspense = |into: &mut String| { let mut resolved_chunk = String::new();
renderer.reset_hydration(); // After we replace the placeholder in the dom with javascript, we need to send down the resolved data so that the client can hydrate the node
renderer.render_scope(into, &virtual_dom, scope) let render_suspense = |into: &mut String| {
}; renderer.reset_hydration();
let resolved_data = serialize_server_data(&virtual_dom, scope); renderer.render_scope(into, &virtual_dom, scope)
if let Err(err) = stream.replace_placeholder( };
mount, let resolved_data = serialize_server_data(&virtual_dom, scope);
render_suspense, if let Err(err) = stream.replace_placeholder(
resolved_data, mount,
&mut resolved_chunk, render_suspense,
) { resolved_data,
throw_error!( &mut resolved_chunk,
dioxus_ssr::incremental::IncrementalRendererError::RenderError(err) ) {
); throw_error!(
} dioxus_ssr::incremental::IncrementalRendererError::RenderError(err)
);
}
stream.render(resolved_chunk); stream.render(resolved_chunk);
}
// Freeze the suspense boundary to prevent future reruns of any child nodes of the suspense boundary
if let Some(suspense) = SuspenseContext::downcast_suspense_boundary_from_scope(
&virtual_dom.runtime(),
scope,
) {
suspense.freeze();
}
} }
} }
+4 -2
View File
@@ -226,8 +226,10 @@ impl WebsysDom {
to_mount: &mut Vec<ElementId>, to_mount: &mut Vec<ElementId>,
) -> Result<(), RehydrationError> { ) -> Result<(), RehydrationError> {
// If this scope is a suspense boundary that is pending, add it to the list of pending suspense boundaries // If this scope is a suspense boundary that is pending, add it to the list of pending suspense boundaries
if let Some(suspense) = SuspenseBoundaryProps::downcast_from_scope(scope) { if let Some(suspense) =
if suspense.suspended() { SuspenseContext::downcast_suspense_boundary_from_scope(&dom.runtime(), scope.id())
{
if suspense.has_suspended_tasks() {
self.suspense_hydration_ids self.suspense_hydration_ids
.add_suspense_boundary(scope.id()); .add_suspense_boundary(scope.id());
} }