Wait for the suspense boundary above the router to resolve before sending the first streaming chunk (#3891)

* Change the suspense streaming context after suspense is resolved in the router

* Wait for the suspense context before streaming the first chunk in ssr

* forward more response parts

* warn

* extract the hydration format into a crate

* Fix hydration when no data is sent

* forward web and server features

* Fix suspense that starts on the client

* fix use_server_future on the client

* Add documentation for fullstack hooks and protocol

* downgrade rust edition

* fix clippy

* fix tests

* fix clippy

* Fix pure web compile

* undo changes to the fullstack router example

* fix formatting
This commit is contained in:
Evan Almloff authored and GitHub committed 2025-04-08 16:01:21 -05:00
1 parent 4f74ef81d1
commit 84221f55a0
45 files changed
+988 -533

No files matched your search

+14 -5
View File
@@ -360,7 +360,7 @@ impl SuspenseBoundaryProps {
SuspenseContext::downcast_suspense_boundary_from_scope(&dom.runtime, scope_id)
.unwrap();
suspense_context.take_suspended_nodes();
mark_suspense_resolved(dom, scope_id);
mark_suspense_resolved(&suspense_context, dom, scope_id);
nodes_created
};
@@ -437,6 +437,9 @@ impl SuspenseBoundaryProps {
let props = Self::downcast_from_props(&mut *scope_state.props).unwrap();
props.children.clone_from(&children);
scope_state.last_rendered_node = Some(children);
// Run any closures that were waiting for the suspense to resolve
suspense_context.run_resolved_closures(&dom.runtime);
})
}
@@ -571,7 +574,7 @@ impl SuspenseBoundaryProps {
// Set the last rendered node to the new children
dom.scopes[scope_id.0].last_rendered_node = Some(new_children);
mark_suspense_resolved(dom, scope_id);
mark_suspense_resolved(&suspense_context, dom, scope_id);
}
}
})
@@ -579,8 +582,14 @@ impl SuspenseBoundaryProps {
}
/// Move to a resolved suspense state
fn mark_suspense_resolved(dom: &mut VirtualDom, scope_id: ScopeId) {
fn mark_suspense_resolved(
suspense_context: &SuspenseContext,
dom: &mut VirtualDom,
scope_id: ScopeId,
) {
dom.resolved_scopes.push(scope_id);
// Run any closures that were waiting for the suspense to resolve
suspense_context.run_resolved_closures(&dom.runtime);
}
/// Move from a resolved suspense state to an suspended state
@@ -590,12 +599,12 @@ fn un_resolve_suspense(dom: &mut VirtualDom, scope_id: ScopeId) {
impl SuspenseContext {
/// Run a closure under a suspense boundary
pub fn under_suspense_boundary<O>(&self, runtime: &Runtime, f: impl FnOnce() -> O) -> O {
pub(crate) 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 {
pub(crate) fn in_suspense_placeholder<O>(&self, runtime: &Runtime, f: impl FnOnce() -> O) -> O {
runtime.with_suspense_location(SuspenseLocation::InSuspensePlaceholder(self.clone()), f)
}
+31 -1
View File
@@ -102,6 +102,7 @@ impl SuspenseContext {
id: Cell::new(ScopeId::ROOT),
suspended_nodes: Default::default(),
frozen: Default::default(),
after_suspense_resolved: Default::default(),
}),
}
}
@@ -184,10 +185,26 @@ impl SuspenseContext {
.find_map(|task| task.suspense_placeholder())
.map(std::result::Result::Ok)
}
/// Run a closure after suspense is resolved
pub fn after_suspense_resolved(&self, callback: impl FnOnce() + 'static) {
let mut closures = self.inner.after_suspense_resolved.borrow_mut();
closures.push(Box::new(callback));
}
/// Run all closures that were queued to run after suspense is resolved
pub(crate) fn run_resolved_closures(&self, runtime: &Runtime) {
runtime.while_not_rendering(|| {
self.inner
.after_suspense_resolved
.borrow_mut()
.drain(..)
.for_each(|f| f());
})
}
}
/// A boundary that will capture any errors from child components
#[derive(Debug)]
pub struct SuspenseBoundaryInner {
suspended_tasks: RefCell<Vec<SuspendedFuture>>,
id: Cell<ScopeId>,
@@ -195,6 +212,19 @@ pub struct SuspenseBoundaryInner {
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>,
/// Closures queued to run after the suspense boundary is resolved
after_suspense_resolved: RefCell<Vec<Box<dyn FnOnce()>>>,
}
impl Debug for SuspenseBoundaryInner {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_struct("SuspenseBoundaryInner")
.field("suspended_tasks", &self.suspended_tasks)
.field("id", &self.id)
.field("suspended_nodes", &self.suspended_nodes)
.field("frozen", &self.frozen)
.finish()
}
}
/// Provides context methods to [`Result<T, RenderError>`] to show loading indicators for suspended results