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:
1 parent
88af3e7eff
commit
e794be5d78
14 files changed
+409
-313
No files matched your search
@@ -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()),
|
||||||
|
|||||||
@@ -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);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -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() {
|
||||||
|
|||||||
@@ -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")
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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())
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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.
|
||||||
|
|||||||
@@ -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)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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
@@ -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);
|
||||||
|
|
||||||
|
|||||||
@@ -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() {
|
||||||
|
|||||||
@@ -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();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -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());
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in new issue
Block a user