mirror of
https://github.com/yewstack/yew.git
synced 2025-12-08 21:26:25 +00:00
Delay Hydration second render until all assistive nodes have been removed (#2629)
* Separate hydration and render queue. * Revert "Fix issue with node refs and hydration (#2597)" This reverts commit 469cc341c340bd0093d9233847f523b66a18fd90. * Priority Render. * Add some tests. * Add more tests. * Add test result after click. * Fix test comment. * Fix test timing. * Restore test. * Once AtomicBool, now a Cell. * Prefer use_future. * Revealing of Suspense always happen after the component has re-rendered itself. * Shifting should register correct next_sibling. * Revert to HashMap. * cargo +nightly fmt. * Fix comment. * Optimise Code size? * Add comment if assertion fails. * Revert "Merge branch 'hydration-4' into fc-prepared-state" This reverts commit 427b087d4db6b2e497ad618273655bd18ba9bd01, reversing changes made to 109fcfaa127aefc5fa3c697e254fe2c049292be2. * Revert "Revert "Merge branch 'hydration-4' into fc-prepared-state"" This reverts commit f1e408958d94cb13813ce75aa6f0aad06c9fa3e8. * Redo #2957.
This commit is contained in:
parent
2db4c81ad6
commit
2576372e26
@ -7,7 +7,7 @@ license = "MIT OR Apache-2.0"
|
|||||||
|
|
||||||
[dependencies]
|
[dependencies]
|
||||||
wasm-bindgen = "0.2"
|
wasm-bindgen = "0.2"
|
||||||
yew = { path = "../../packages/yew", features = ["csr"] }
|
yew = { path = "../../packages/yew", features = ["csr", "tokio"] }
|
||||||
wasm-bindgen-futures = "0.4"
|
wasm-bindgen-futures = "0.4"
|
||||||
js-sys = "0.3"
|
js-sys = "0.3"
|
||||||
once_cell = "1"
|
once_cell = "1"
|
||||||
|
|||||||
@ -44,8 +44,10 @@ impl ReconcileTarget for BComp {
|
|||||||
self.scope.destroy_boxed(parent_to_detach);
|
self.scope.destroy_boxed(parent_to_detach);
|
||||||
}
|
}
|
||||||
|
|
||||||
fn shift(&self, next_parent: &Element, next_sibling: NodeRef) {
|
fn shift(&self, next_parent: &Element, next_sibling: NodeRef) -> NodeRef {
|
||||||
self.scope.shift_node(next_parent.clone(), next_sibling);
|
self.scope.shift_node(next_parent.clone(), next_sibling);
|
||||||
|
|
||||||
|
self.node_ref.clone()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -380,10 +380,14 @@ impl ReconcileTarget for BList {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn shift(&self, next_parent: &Element, next_sibling: NodeRef) {
|
fn shift(&self, next_parent: &Element, next_sibling: NodeRef) -> NodeRef {
|
||||||
for node in self.rev_children.iter().rev() {
|
let mut next_sibling = next_sibling;
|
||||||
node.shift(next_parent, next_sibling.clone());
|
|
||||||
|
for node in self.rev_children.iter() {
|
||||||
|
next_sibling = node.shift(next_parent, next_sibling.clone());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
next_sibling
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -62,7 +62,7 @@ impl ReconcileTarget for BNode {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn shift(&self, next_parent: &Element, next_sibling: NodeRef) {
|
fn shift(&self, next_parent: &Element, next_sibling: NodeRef) -> NodeRef {
|
||||||
match self {
|
match self {
|
||||||
Self::Tag(ref vtag) => vtag.shift(next_parent, next_sibling),
|
Self::Tag(ref vtag) => vtag.shift(next_parent, next_sibling),
|
||||||
Self::Text(ref btext) => btext.shift(next_parent, next_sibling),
|
Self::Text(ref btext) => btext.shift(next_parent, next_sibling),
|
||||||
@ -72,6 +72,8 @@ impl ReconcileTarget for BNode {
|
|||||||
next_parent
|
next_parent
|
||||||
.insert_before(node, next_sibling.get().as_ref())
|
.insert_before(node, next_sibling.get().as_ref())
|
||||||
.unwrap();
|
.unwrap();
|
||||||
|
|
||||||
|
NodeRef::new(node.clone())
|
||||||
}
|
}
|
||||||
Self::Portal(ref vportal) => vportal.shift(next_parent, next_sibling),
|
Self::Portal(ref vportal) => vportal.shift(next_parent, next_sibling),
|
||||||
Self::Suspense(ref vsuspense) => vsuspense.shift(next_parent, next_sibling),
|
Self::Suspense(ref vsuspense) => vsuspense.shift(next_parent, next_sibling),
|
||||||
|
|||||||
@ -26,8 +26,10 @@ impl ReconcileTarget for BPortal {
|
|||||||
self.node.detach(&self.inner_root, &self.host, false);
|
self.node.detach(&self.inner_root, &self.host, false);
|
||||||
}
|
}
|
||||||
|
|
||||||
fn shift(&self, _next_parent: &Element, _next_sibling: NodeRef) {
|
fn shift(&self, _next_parent: &Element, next_sibling: NodeRef) -> NodeRef {
|
||||||
// portals have nothing in it's original place of DOM, we also do nothing.
|
// portals have nothing in it's original place of DOM, we also do nothing.
|
||||||
|
|
||||||
|
next_sibling
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -60,18 +60,12 @@ impl ReconcileTarget for BSuspense {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn shift(&self, next_parent: &Element, next_sibling: NodeRef) {
|
fn shift(&self, next_parent: &Element, next_sibling: NodeRef) -> NodeRef {
|
||||||
match self.fallback.as_ref() {
|
match self.fallback.as_ref() {
|
||||||
Some(Fallback::Bundle(bundle)) => {
|
Some(Fallback::Bundle(bundle)) => bundle.shift(next_parent, next_sibling),
|
||||||
bundle.shift(next_parent, next_sibling);
|
|
||||||
}
|
|
||||||
#[cfg(feature = "hydration")]
|
#[cfg(feature = "hydration")]
|
||||||
Some(Fallback::Fragment(fragment)) => {
|
Some(Fallback::Fragment(fragment)) => fragment.shift(next_parent, next_sibling),
|
||||||
fragment.shift(next_parent, next_sibling);
|
None => self.children_bundle.shift(next_parent, next_sibling),
|
||||||
}
|
|
||||||
None => {
|
|
||||||
self.children_bundle.shift(next_parent, next_sibling);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -94,10 +94,12 @@ impl ReconcileTarget for BTag {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn shift(&self, next_parent: &Element, next_sibling: NodeRef) {
|
fn shift(&self, next_parent: &Element, next_sibling: NodeRef) -> NodeRef {
|
||||||
next_parent
|
next_parent
|
||||||
.insert_before(&self.reference, next_sibling.get().as_ref())
|
.insert_before(&self.reference, next_sibling.get().as_ref())
|
||||||
.unwrap();
|
.unwrap();
|
||||||
|
|
||||||
|
self.node_ref.clone()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -26,12 +26,14 @@ impl ReconcileTarget for BText {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn shift(&self, next_parent: &Element, next_sibling: NodeRef) {
|
fn shift(&self, next_parent: &Element, next_sibling: NodeRef) -> NodeRef {
|
||||||
let node = &self.text_node;
|
let node = &self.text_node;
|
||||||
|
|
||||||
next_parent
|
next_parent
|
||||||
.insert_before(node, next_sibling.get().as_ref())
|
.insert_before(node, next_sibling.get().as_ref())
|
||||||
.unwrap();
|
.unwrap();
|
||||||
|
|
||||||
|
NodeRef::new(self.text_node.clone().into())
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -156,11 +156,16 @@ impl Fragment {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// Shift current Fragment into a different position in the dom.
|
/// Shift current Fragment into a different position in the dom.
|
||||||
pub fn shift(&self, next_parent: &Element, next_sibling: NodeRef) {
|
pub fn shift(&self, next_parent: &Element, next_sibling: NodeRef) -> NodeRef {
|
||||||
for node in self.iter() {
|
for node in self.iter() {
|
||||||
next_parent
|
next_parent
|
||||||
.insert_before(node, next_sibling.get().as_ref())
|
.insert_before(node, next_sibling.get().as_ref())
|
||||||
.unwrap();
|
.unwrap();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
self.front()
|
||||||
|
.cloned()
|
||||||
|
.map(NodeRef::new)
|
||||||
|
.unwrap_or(next_sibling)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -16,7 +16,7 @@ pub(super) trait ReconcileTarget {
|
|||||||
/// Move elements from one parent to another parent.
|
/// Move elements from one parent to another parent.
|
||||||
/// This is for example used by `VSuspense` to preserve component state without detaching
|
/// This is for example used by `VSuspense` to preserve component state without detaching
|
||||||
/// (which destroys component state).
|
/// (which destroys component state).
|
||||||
fn shift(&self, next_parent: &Element, next_sibling: NodeRef);
|
fn shift(&self, next_parent: &Element, next_sibling: NodeRef) -> NodeRef;
|
||||||
}
|
}
|
||||||
|
|
||||||
/// This trait provides features to update a tree by calculating a difference against another tree.
|
/// This trait provides features to update a tree by calculating a difference against another tree.
|
||||||
|
|||||||
@ -301,61 +301,83 @@ impl<COMP: BaseComponent> Runnable for CreateRunner<COMP> {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub(crate) enum UpdateEvent {
|
#[cfg(feature = "csr")]
|
||||||
/// Drain messages for a component.
|
pub(crate) struct PropsUpdateRunner {
|
||||||
Message,
|
pub props: Rc<dyn Any>,
|
||||||
/// Wraps properties, node ref, and next sibling for a component
|
pub state: Shared<Option<ComponentState>>,
|
||||||
#[cfg(feature = "csr")]
|
pub next_sibling: NodeRef,
|
||||||
Properties(Rc<dyn Any>, NodeRef),
|
}
|
||||||
|
|
||||||
|
#[cfg(feature = "csr")]
|
||||||
|
impl Runnable for PropsUpdateRunner {
|
||||||
|
fn run(self: Box<Self>) {
|
||||||
|
let Self {
|
||||||
|
next_sibling,
|
||||||
|
props,
|
||||||
|
state: shared_state,
|
||||||
|
} = *self;
|
||||||
|
|
||||||
|
if let Some(state) = shared_state.borrow_mut().as_mut() {
|
||||||
|
let schedule_render = match state.render_state {
|
||||||
|
#[cfg(feature = "csr")]
|
||||||
|
ComponentRenderState::Render {
|
||||||
|
next_sibling: ref mut current_next_sibling,
|
||||||
|
..
|
||||||
|
} => {
|
||||||
|
// When components are updated, their siblings were likely also updated
|
||||||
|
*current_next_sibling = next_sibling;
|
||||||
|
// Only trigger changed if props were changed
|
||||||
|
state.inner.props_changed(props)
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(feature = "hydration")]
|
||||||
|
ComponentRenderState::Hydration {
|
||||||
|
next_sibling: ref mut current_next_sibling,
|
||||||
|
..
|
||||||
|
} => {
|
||||||
|
// When components are updated, their siblings were likely also updated
|
||||||
|
*current_next_sibling = next_sibling;
|
||||||
|
// Only trigger changed if props were changed
|
||||||
|
state.inner.props_changed(props)
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(feature = "ssr")]
|
||||||
|
ComponentRenderState::Ssr { .. } => {
|
||||||
|
#[cfg(debug_assertions)]
|
||||||
|
panic!("properties do not change during SSR");
|
||||||
|
|
||||||
|
#[cfg(not(debug_assertions))]
|
||||||
|
false
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
#[cfg(debug_assertions)]
|
||||||
|
super::log_event(
|
||||||
|
state.comp_id,
|
||||||
|
format!("props_update(schedule_render={})", schedule_render),
|
||||||
|
);
|
||||||
|
|
||||||
|
if schedule_render {
|
||||||
|
scheduler::push_component_render(
|
||||||
|
state.comp_id,
|
||||||
|
Box::new(RenderRunner {
|
||||||
|
state: shared_state.clone(),
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
// Only run from the scheduler, so no need to call `scheduler::start()`
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub(crate) struct UpdateRunner {
|
pub(crate) struct UpdateRunner {
|
||||||
pub state: Shared<Option<ComponentState>>,
|
pub state: Shared<Option<ComponentState>>,
|
||||||
pub event: UpdateEvent,
|
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Runnable for UpdateRunner {
|
impl Runnable for UpdateRunner {
|
||||||
fn run(self: Box<Self>) {
|
fn run(self: Box<Self>) {
|
||||||
if let Some(state) = self.state.borrow_mut().as_mut() {
|
if let Some(state) = self.state.borrow_mut().as_mut() {
|
||||||
let schedule_render = match self.event {
|
let schedule_render = state.inner.flush_messages();
|
||||||
UpdateEvent::Message => state.inner.flush_messages(),
|
|
||||||
|
|
||||||
#[cfg(feature = "csr")]
|
|
||||||
UpdateEvent::Properties(props, next_sibling) => {
|
|
||||||
match state.render_state {
|
|
||||||
#[cfg(feature = "csr")]
|
|
||||||
ComponentRenderState::Render {
|
|
||||||
next_sibling: ref mut current_next_sibling,
|
|
||||||
..
|
|
||||||
} => {
|
|
||||||
// When components are updated, their siblings were likely also updated
|
|
||||||
*current_next_sibling = next_sibling;
|
|
||||||
// Only trigger changed if props were changed
|
|
||||||
state.inner.props_changed(props)
|
|
||||||
}
|
|
||||||
|
|
||||||
#[cfg(feature = "hydration")]
|
|
||||||
ComponentRenderState::Hydration {
|
|
||||||
next_sibling: ref mut current_next_sibling,
|
|
||||||
..
|
|
||||||
} => {
|
|
||||||
// When components are updated, their siblings were likely also updated
|
|
||||||
*current_next_sibling = next_sibling;
|
|
||||||
// Only trigger changed if props were changed
|
|
||||||
state.inner.props_changed(props)
|
|
||||||
}
|
|
||||||
|
|
||||||
#[cfg(feature = "ssr")]
|
|
||||||
ComponentRenderState::Ssr { .. } => {
|
|
||||||
#[cfg(debug_assertions)]
|
|
||||||
panic!("properties do not change during SSR");
|
|
||||||
|
|
||||||
#[cfg(not(debug_assertions))]
|
|
||||||
false
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
#[cfg(debug_assertions)]
|
#[cfg(debug_assertions)]
|
||||||
super::log_event(
|
super::log_event(
|
||||||
@ -453,9 +475,8 @@ impl RenderRunner {
|
|||||||
|
|
||||||
if suspension.resumed() {
|
if suspension.resumed() {
|
||||||
// schedule a render immediately if suspension is resumed.
|
// schedule a render immediately if suspension is resumed.
|
||||||
|
|
||||||
scheduler::push_component_render(
|
scheduler::push_component_render(
|
||||||
state.comp_id,
|
comp_id,
|
||||||
Box::new(RenderRunner {
|
Box::new(RenderRunner {
|
||||||
state: shared_state,
|
state: shared_state,
|
||||||
}),
|
}),
|
||||||
@ -542,7 +563,7 @@ impl RenderRunner {
|
|||||||
} => {
|
} => {
|
||||||
// We schedule a "first" render to run immediately after hydration,
|
// We schedule a "first" render to run immediately after hydration,
|
||||||
// to fix NodeRefs (first_node and next_sibling).
|
// to fix NodeRefs (first_node and next_sibling).
|
||||||
scheduler::push_component_first_render(
|
scheduler::push_component_priority_render(
|
||||||
state.comp_id,
|
state.comp_id,
|
||||||
Box::new(RenderRunner {
|
Box::new(RenderRunner {
|
||||||
state: self.state.clone(),
|
state: self.state.clone(),
|
||||||
|
|||||||
@ -9,7 +9,7 @@ use std::rc::Rc;
|
|||||||
use std::{fmt, iter};
|
use std::{fmt, iter};
|
||||||
|
|
||||||
#[cfg(any(feature = "csr", feature = "ssr"))]
|
#[cfg(any(feature = "csr", feature = "ssr"))]
|
||||||
use super::lifecycle::{ComponentState, UpdateEvent, UpdateRunner};
|
use super::lifecycle::{ComponentState, UpdateRunner};
|
||||||
use super::BaseComponent;
|
use super::BaseComponent;
|
||||||
use crate::callback::Callback;
|
use crate::callback::Callback;
|
||||||
use crate::context::{ContextHandle, ContextProvider};
|
use crate::context::{ContextHandle, ContextProvider};
|
||||||
@ -353,10 +353,10 @@ mod feat_csr_ssr {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
pub(super) fn push_update(&self, event: UpdateEvent) {
|
#[inline]
|
||||||
|
fn schedule_update(&self) {
|
||||||
scheduler::push_component_update(Box::new(UpdateRunner {
|
scheduler::push_component_update(Box::new(UpdateRunner {
|
||||||
state: self.state.clone(),
|
state: self.state.clone(),
|
||||||
event,
|
|
||||||
}));
|
}));
|
||||||
// Not guaranteed to already have the scheduler started
|
// Not guaranteed to already have the scheduler started
|
||||||
scheduler::start();
|
scheduler::start();
|
||||||
@ -369,7 +369,7 @@ mod feat_csr_ssr {
|
|||||||
{
|
{
|
||||||
// We are the first message in queue, so we queue the update.
|
// We are the first message in queue, so we queue the update.
|
||||||
if self.pending_messages.push(msg.into()) == 1 {
|
if self.pending_messages.push(msg.into()) == 1 {
|
||||||
self.push_update(UpdateEvent::Message);
|
self.schedule_update();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -382,7 +382,7 @@ mod feat_csr_ssr {
|
|||||||
|
|
||||||
// The queue was empty, so we queue the update
|
// The queue was empty, so we queue the update
|
||||||
if self.pending_messages.append(&mut messages) == msg_len {
|
if self.pending_messages.append(&mut messages) == msg_len {
|
||||||
self.push_update(UpdateEvent::Message);
|
self.schedule_update();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -400,7 +400,7 @@ mod feat_csr {
|
|||||||
use super::*;
|
use super::*;
|
||||||
use crate::dom_bundle::{BSubtree, Bundle};
|
use crate::dom_bundle::{BSubtree, Bundle};
|
||||||
use crate::html::component::lifecycle::{
|
use crate::html::component::lifecycle::{
|
||||||
ComponentRenderState, CreateRunner, DestroyRunner, RenderRunner,
|
ComponentRenderState, CreateRunner, DestroyRunner, PropsUpdateRunner, RenderRunner,
|
||||||
};
|
};
|
||||||
use crate::html::NodeRef;
|
use crate::html::NodeRef;
|
||||||
use crate::scheduler;
|
use crate::scheduler;
|
||||||
@ -416,6 +416,20 @@ mod feat_csr {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn schedule_props_update(
|
||||||
|
state: Shared<Option<ComponentState>>,
|
||||||
|
props: Rc<dyn Any>,
|
||||||
|
next_sibling: NodeRef,
|
||||||
|
) {
|
||||||
|
scheduler::push_component_props_update(Box::new(PropsUpdateRunner {
|
||||||
|
state,
|
||||||
|
next_sibling,
|
||||||
|
props,
|
||||||
|
}));
|
||||||
|
// Not guaranteed to already have the scheduler started
|
||||||
|
scheduler::start();
|
||||||
|
}
|
||||||
|
|
||||||
impl<COMP> Scope<COMP>
|
impl<COMP> Scope<COMP>
|
||||||
where
|
where
|
||||||
COMP: BaseComponent,
|
COMP: BaseComponent,
|
||||||
@ -459,7 +473,7 @@ mod feat_csr {
|
|||||||
#[cfg(debug_assertions)]
|
#[cfg(debug_assertions)]
|
||||||
super::super::log_event(self.id, "reuse");
|
super::super::log_event(self.id, "reuse");
|
||||||
|
|
||||||
self.push_update(UpdateEvent::Properties(props, next_sibling));
|
schedule_props_update(self.state.clone(), props, next_sibling)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -23,6 +23,8 @@ struct Scheduler {
|
|||||||
// Component queues
|
// Component queues
|
||||||
destroy: Vec<Box<dyn Runnable>>,
|
destroy: Vec<Box<dyn Runnable>>,
|
||||||
create: Vec<Box<dyn Runnable>>,
|
create: Vec<Box<dyn Runnable>>,
|
||||||
|
|
||||||
|
props_update: Vec<Box<dyn Runnable>>,
|
||||||
update: Vec<Box<dyn Runnable>>,
|
update: Vec<Box<dyn Runnable>>,
|
||||||
|
|
||||||
/// The Binary Tree Map guarantees components with lower id (parent) is rendered first and
|
/// The Binary Tree Map guarantees components with lower id (parent) is rendered first and
|
||||||
@ -30,8 +32,10 @@ struct Scheduler {
|
|||||||
///
|
///
|
||||||
/// Parent can destroy child components but not otherwise, we can save unnecessary render by
|
/// Parent can destroy child components but not otherwise, we can save unnecessary render by
|
||||||
/// rendering parent first.
|
/// rendering parent first.
|
||||||
render_first: BTreeMap<usize, Box<dyn Runnable>>,
|
|
||||||
render: BTreeMap<usize, Box<dyn Runnable>>,
|
render: BTreeMap<usize, Box<dyn Runnable>>,
|
||||||
|
render_first: BTreeMap<usize, Box<dyn Runnable>>,
|
||||||
|
#[cfg(feature = "hydration")]
|
||||||
|
render_priority: BTreeMap<usize, Box<dyn Runnable>>,
|
||||||
|
|
||||||
/// Binary Tree Map to guarantee children rendered are always called before parent calls
|
/// Binary Tree Map to guarantee children rendered are always called before parent calls
|
||||||
rendered_first: BTreeMap<usize, Box<dyn Runnable>>,
|
rendered_first: BTreeMap<usize, Box<dyn Runnable>>,
|
||||||
@ -113,21 +117,26 @@ mod feat_csr {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
#[cfg(feature = "hydration")]
|
pub(crate) fn push_component_props_update(props_update: Box<dyn Runnable>) {
|
||||||
mod feat_hydration {
|
with(|s| s.props_update.push(props_update));
|
||||||
use super::*;
|
|
||||||
|
|
||||||
pub(crate) fn push_component_first_render(component_id: usize, render: Box<dyn Runnable>) {
|
|
||||||
with(|s| {
|
|
||||||
s.render_first.insert(component_id, render);
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[cfg(feature = "csr")]
|
#[cfg(feature = "csr")]
|
||||||
pub(crate) use feat_csr::*;
|
pub(crate) use feat_csr::*;
|
||||||
|
|
||||||
|
#[cfg(feature = "hydration")]
|
||||||
|
mod feat_hydration {
|
||||||
|
use super::*;
|
||||||
|
|
||||||
|
pub(crate) fn push_component_priority_render(component_id: usize, render: Box<dyn Runnable>) {
|
||||||
|
with(|s| {
|
||||||
|
s.render_priority.insert(component_id, render);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
#[cfg(feature = "hydration")]
|
#[cfg(feature = "hydration")]
|
||||||
pub(crate) use feat_hydration::*;
|
pub(crate) use feat_hydration::*;
|
||||||
|
|
||||||
@ -226,12 +235,32 @@ impl Scheduler {
|
|||||||
to_run.push(r);
|
to_run.push(r);
|
||||||
}
|
}
|
||||||
|
|
||||||
// These typically do nothing and don't spawn any other events - can be batched.
|
|
||||||
// Should be run only after all first renders have finished.
|
|
||||||
if !to_run.is_empty() {
|
if !to_run.is_empty() {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
to_run.append(&mut self.props_update);
|
||||||
|
|
||||||
|
// Priority rendering
|
||||||
|
//
|
||||||
|
// This is needed for hydration susequent render to fix node refs.
|
||||||
|
#[cfg(feature = "hydration")]
|
||||||
|
{
|
||||||
|
if let Some(r) = self
|
||||||
|
.render_priority
|
||||||
|
.keys()
|
||||||
|
.next()
|
||||||
|
.cloned()
|
||||||
|
.and_then(|m| self.render_priority.remove(&m))
|
||||||
|
{
|
||||||
|
to_run.push(r);
|
||||||
|
}
|
||||||
|
|
||||||
|
if !to_run.is_empty() {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
if !self.rendered_first.is_empty() {
|
if !self.rendered_first.is_empty() {
|
||||||
let rendered_first = std::mem::take(&mut self.rendered_first);
|
let rendered_first = std::mem::take(&mut self.rendered_first);
|
||||||
// Children rendered lifecycle happen before parents.
|
// Children rendered lifecycle happen before parents.
|
||||||
|
|||||||
@ -90,8 +90,6 @@ mod feat_csr_ssr {
|
|||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
m.listen(self.link.callback(Self::Message::Resume));
|
|
||||||
|
|
||||||
self.suspensions.push(m);
|
self.suspensions.push(m);
|
||||||
|
|
||||||
true
|
true
|
||||||
|
|||||||
@ -12,11 +12,14 @@ use wasm_bindgen_futures::spawn_local;
|
|||||||
use wasm_bindgen_test::*;
|
use wasm_bindgen_test::*;
|
||||||
use web_sys::{HtmlElement, HtmlTextAreaElement};
|
use web_sys::{HtmlElement, HtmlTextAreaElement};
|
||||||
use yew::prelude::*;
|
use yew::prelude::*;
|
||||||
use yew::suspense::{Suspension, SuspensionResult};
|
use yew::suspense::{use_future, Suspension, SuspensionResult};
|
||||||
use yew::{Renderer, ServerRenderer};
|
use yew::{Renderer, ServerRenderer};
|
||||||
|
|
||||||
wasm_bindgen_test::wasm_bindgen_test_configure!(run_in_browser);
|
wasm_bindgen_test::wasm_bindgen_test_configure!(run_in_browser);
|
||||||
|
|
||||||
|
// If any of the assertions fail due to a modification to hydration logic, cargo will suggest the
|
||||||
|
// expected result and you can copy it into the test to fix it.
|
||||||
|
|
||||||
#[wasm_bindgen_test]
|
#[wasm_bindgen_test]
|
||||||
async fn hydration_works() {
|
async fn hydration_works() {
|
||||||
#[function_component]
|
#[function_component]
|
||||||
@ -539,3 +542,373 @@ async fn hydration_nested_suspense_works() {
|
|||||||
r#"<div class="content-area"><div class="action-area"><button class="take-a-break">Take a break!</button></div><div class="content-area"><div class="action-area"><button class="take-a-break2">Take a break!</button></div></div></div>"#
|
r#"<div class="content-area"><div class="action-area"><button class="take-a-break">Take a break!</button></div><div class="content-area"><div class="action-area"><button class="take-a-break2">Take a break!</button></div></div></div>"#
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[wasm_bindgen_test]
|
||||||
|
async fn hydration_node_ref_works() {
|
||||||
|
#[function_component(App)]
|
||||||
|
pub fn app() -> Html {
|
||||||
|
let size = use_state(|| 4);
|
||||||
|
|
||||||
|
let callback = {
|
||||||
|
let size = size.clone();
|
||||||
|
Callback::from(move |_| {
|
||||||
|
size.set(10);
|
||||||
|
})
|
||||||
|
};
|
||||||
|
|
||||||
|
html! {
|
||||||
|
<div onclick={callback}>
|
||||||
|
<List size={*size}/>
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Properties, PartialEq)]
|
||||||
|
struct ListProps {
|
||||||
|
size: u32,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[function_component(Test1)]
|
||||||
|
fn test1() -> Html {
|
||||||
|
html! {
|
||||||
|
<span>{"test"}</span>
|
||||||
|
}
|
||||||
|
}
|
||||||
|
#[function_component(Test2)]
|
||||||
|
fn test2() -> Html {
|
||||||
|
html! {
|
||||||
|
<Test1/>
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[function_component(List)]
|
||||||
|
fn list(props: &ListProps) -> Html {
|
||||||
|
let elems = 0..props.size;
|
||||||
|
|
||||||
|
html! {
|
||||||
|
<>
|
||||||
|
{ for elems.map(|_|
|
||||||
|
html! {
|
||||||
|
<Test2/>
|
||||||
|
}
|
||||||
|
)}
|
||||||
|
</>
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
let s = ServerRenderer::<App>::new().render().await;
|
||||||
|
|
||||||
|
gloo::utils::document()
|
||||||
|
.query_selector("#output")
|
||||||
|
.unwrap()
|
||||||
|
.unwrap()
|
||||||
|
.set_inner_html(&s);
|
||||||
|
|
||||||
|
sleep(Duration::ZERO).await;
|
||||||
|
|
||||||
|
Renderer::<App>::with_root(gloo_utils::document().get_element_by_id("output").unwrap())
|
||||||
|
.hydrate();
|
||||||
|
|
||||||
|
sleep(Duration::ZERO).await;
|
||||||
|
|
||||||
|
let result = obtain_result_by_id("output");
|
||||||
|
assert_eq!(
|
||||||
|
result.as_str(),
|
||||||
|
r#"<div><span>test</span><span>test</span><span>test</span><span>test</span></div>"#
|
||||||
|
);
|
||||||
|
|
||||||
|
gloo_utils::document()
|
||||||
|
.query_selector("span")
|
||||||
|
.unwrap()
|
||||||
|
.unwrap()
|
||||||
|
.dyn_into::<HtmlElement>()
|
||||||
|
.unwrap()
|
||||||
|
.click();
|
||||||
|
|
||||||
|
sleep(Duration::ZERO).await;
|
||||||
|
|
||||||
|
let result = obtain_result_by_id("output");
|
||||||
|
assert_eq!(
|
||||||
|
result.as_str(),
|
||||||
|
r#"<div><span>test</span><span>test</span><span>test</span><span>test</span><span>test</span><span>test</span><span>test</span><span>test</span><span>test</span><span>test</span></div>"#
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[wasm_bindgen_test]
|
||||||
|
async fn hydration_list_order_works() {
|
||||||
|
#[function_component(App)]
|
||||||
|
pub fn app() -> Html {
|
||||||
|
let elems = 0..10;
|
||||||
|
|
||||||
|
html! {
|
||||||
|
<>
|
||||||
|
{ for elems.map(|number|
|
||||||
|
html! {
|
||||||
|
<ToSuspendOrNot {number}/>
|
||||||
|
}
|
||||||
|
)}
|
||||||
|
</>
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Properties, PartialEq)]
|
||||||
|
struct NumberProps {
|
||||||
|
number: u32,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[function_component(Number)]
|
||||||
|
fn number(props: &NumberProps) -> Html {
|
||||||
|
html! {
|
||||||
|
<div>{props.number.to_string()}</div>
|
||||||
|
}
|
||||||
|
}
|
||||||
|
#[function_component(SuspendedNumber)]
|
||||||
|
fn suspended_number(props: &NumberProps) -> HtmlResult {
|
||||||
|
use_suspend()?;
|
||||||
|
Ok(html! {
|
||||||
|
<div>{props.number.to_string()}</div>
|
||||||
|
})
|
||||||
|
}
|
||||||
|
#[function_component(ToSuspendOrNot)]
|
||||||
|
fn suspend_or_not(props: &NumberProps) -> Html {
|
||||||
|
let number = props.number;
|
||||||
|
html! {
|
||||||
|
<Suspense>
|
||||||
|
if number % 3 == 0 {
|
||||||
|
<SuspendedNumber {number}/>
|
||||||
|
} else {
|
||||||
|
<Number {number}/>
|
||||||
|
}
|
||||||
|
</Suspense>
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[hook]
|
||||||
|
pub fn use_suspend() -> SuspensionResult<()> {
|
||||||
|
use_future(|| async {})?;
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
let s = ServerRenderer::<App>::new().render().await;
|
||||||
|
|
||||||
|
gloo::utils::document()
|
||||||
|
.query_selector("#output")
|
||||||
|
.unwrap()
|
||||||
|
.unwrap()
|
||||||
|
.set_inner_html(&s);
|
||||||
|
|
||||||
|
sleep(Duration::ZERO).await;
|
||||||
|
|
||||||
|
Renderer::<App>::with_root(gloo_utils::document().get_element_by_id("output").unwrap())
|
||||||
|
.hydrate();
|
||||||
|
|
||||||
|
// Wait until all suspended components becomes revealed.
|
||||||
|
sleep(Duration::ZERO).await;
|
||||||
|
sleep(Duration::ZERO).await;
|
||||||
|
sleep(Duration::ZERO).await;
|
||||||
|
sleep(Duration::ZERO).await;
|
||||||
|
|
||||||
|
let result = obtain_result_by_id("output");
|
||||||
|
assert_eq!(
|
||||||
|
result.as_str(),
|
||||||
|
// Until all components become revealed, there will be component markers.
|
||||||
|
// As long as there's no component markers all components have become unsuspended.
|
||||||
|
r#"<div>0</div><div>1</div><div>2</div><div>3</div><div>4</div><div>5</div><div>6</div><div>7</div><div>8</div><div>9</div>"#
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[wasm_bindgen_test]
|
||||||
|
async fn hydration_suspense_no_flickering() {
|
||||||
|
#[function_component(App)]
|
||||||
|
pub fn app() -> Html {
|
||||||
|
let fallback = html! { <h1>{"Loading..."}</h1> };
|
||||||
|
html! {
|
||||||
|
<Suspense {fallback}>
|
||||||
|
<Suspended/>
|
||||||
|
</Suspense>
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Properties, PartialEq, Clone)]
|
||||||
|
struct NumberProps {
|
||||||
|
number: u32,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[function_component(SuspendedNumber)]
|
||||||
|
fn suspended_number(props: &NumberProps) -> HtmlResult {
|
||||||
|
use_suspend()?;
|
||||||
|
|
||||||
|
Ok(html! {
|
||||||
|
<Number ..{props.clone()}/>
|
||||||
|
})
|
||||||
|
}
|
||||||
|
#[function_component(Number)]
|
||||||
|
fn number(props: &NumberProps) -> Html {
|
||||||
|
html! {
|
||||||
|
<div>
|
||||||
|
{props.number.to_string()}
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[function_component(Suspended)]
|
||||||
|
fn suspended() -> HtmlResult {
|
||||||
|
use_suspend()?;
|
||||||
|
|
||||||
|
Ok(html! {
|
||||||
|
{ for (0..10).map(|number|
|
||||||
|
html! {
|
||||||
|
<SuspendedNumber {number}/>
|
||||||
|
}
|
||||||
|
)}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
#[hook]
|
||||||
|
pub fn use_suspend() -> SuspensionResult<()> {
|
||||||
|
use_future(|| async {
|
||||||
|
gloo::timers::future::sleep(std::time::Duration::from_millis(50)).await;
|
||||||
|
})?;
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
let s = ServerRenderer::<App>::new().render().await;
|
||||||
|
|
||||||
|
gloo::utils::document()
|
||||||
|
.query_selector("#output")
|
||||||
|
.unwrap()
|
||||||
|
.unwrap()
|
||||||
|
.set_inner_html(&s);
|
||||||
|
|
||||||
|
sleep(Duration::ZERO).await;
|
||||||
|
|
||||||
|
Renderer::<App>::with_root(gloo_utils::document().get_element_by_id("output").unwrap())
|
||||||
|
.hydrate();
|
||||||
|
|
||||||
|
// Wait until all suspended components becomes revealed.
|
||||||
|
sleep(Duration::ZERO).await;
|
||||||
|
|
||||||
|
let result = obtain_result_by_id("output");
|
||||||
|
assert_eq!(
|
||||||
|
result.as_str(),
|
||||||
|
// outer still suspended.
|
||||||
|
r#"<!--<[hydration::hydration_suspense_no_flickering::{{closure}}::Suspended]>--><!--<[hydration::hydration_suspense_no_flickering::{{closure}}::SuspendedNumber]>--><!--<[hydration::hydration_suspense_no_flickering::{{closure}}::Number]>--><div>0</div><!--</[hydration::hydration_suspense_no_flickering::{{closure}}::Number]>--><!--</[hydration::hydration_suspense_no_flickering::{{closure}}::SuspendedNumber]>--><!--<[hydration::hydration_suspense_no_flickering::{{closure}}::SuspendedNumber]>--><!--<[hydration::hydration_suspense_no_flickering::{{closure}}::Number]>--><div>1</div><!--</[hydration::hydration_suspense_no_flickering::{{closure}}::Number]>--><!--</[hydration::hydration_suspense_no_flickering::{{closure}}::SuspendedNumber]>--><!--<[hydration::hydration_suspense_no_flickering::{{closure}}::SuspendedNumber]>--><!--<[hydration::hydration_suspense_no_flickering::{{closure}}::Number]>--><div>2</div><!--</[hydration::hydration_suspense_no_flickering::{{closure}}::Number]>--><!--</[hydration::hydration_suspense_no_flickering::{{closure}}::SuspendedNumber]>--><!--<[hydration::hydration_suspense_no_flickering::{{closure}}::SuspendedNumber]>--><!--<[hydration::hydration_suspense_no_flickering::{{closure}}::Number]>--><div>3</div><!--</[hydration::hydration_suspense_no_flickering::{{closure}}::Number]>--><!--</[hydration::hydration_suspense_no_flickering::{{closure}}::SuspendedNumber]>--><!--<[hydration::hydration_suspense_no_flickering::{{closure}}::SuspendedNumber]>--><!--<[hydration::hydration_suspense_no_flickering::{{closure}}::Number]>--><div>4</div><!--</[hydration::hydration_suspense_no_flickering::{{closure}}::Number]>--><!--</[hydration::hydration_suspense_no_flickering::{{closure}}::SuspendedNumber]>--><!--<[hydration::hydration_suspense_no_flickering::{{closure}}::SuspendedNumber]>--><!--<[hydration::hydration_suspense_no_flickering::{{closure}}::Number]>--><div>5</div><!--</[hydration::hydration_suspense_no_flickering::{{closure}}::Number]>--><!--</[hydration::hydration_suspense_no_flickering::{{closure}}::SuspendedNumber]>--><!--<[hydration::hydration_suspense_no_flickering::{{closure}}::SuspendedNumber]>--><!--<[hydration::hydration_suspense_no_flickering::{{closure}}::Number]>--><div>6</div><!--</[hydration::hydration_suspense_no_flickering::{{closure}}::Number]>--><!--</[hydration::hydration_suspense_no_flickering::{{closure}}::SuspendedNumber]>--><!--<[hydration::hydration_suspense_no_flickering::{{closure}}::SuspendedNumber]>--><!--<[hydration::hydration_suspense_no_flickering::{{closure}}::Number]>--><div>7</div><!--</[hydration::hydration_suspense_no_flickering::{{closure}}::Number]>--><!--</[hydration::hydration_suspense_no_flickering::{{closure}}::SuspendedNumber]>--><!--<[hydration::hydration_suspense_no_flickering::{{closure}}::SuspendedNumber]>--><!--<[hydration::hydration_suspense_no_flickering::{{closure}}::Number]>--><div>8</div><!--</[hydration::hydration_suspense_no_flickering::{{closure}}::Number]>--><!--</[hydration::hydration_suspense_no_flickering::{{closure}}::SuspendedNumber]>--><!--<[hydration::hydration_suspense_no_flickering::{{closure}}::SuspendedNumber]>--><!--<[hydration::hydration_suspense_no_flickering::{{closure}}::Number]>--><div>9</div><!--</[hydration::hydration_suspense_no_flickering::{{closure}}::Number]>--><!--</[hydration::hydration_suspense_no_flickering::{{closure}}::SuspendedNumber]>--><!--</[hydration::hydration_suspense_no_flickering::{{closure}}::Suspended]>-->"#
|
||||||
|
);
|
||||||
|
sleep(Duration::from_millis(26)).await;
|
||||||
|
|
||||||
|
let result = obtain_result_by_id("output");
|
||||||
|
assert_eq!(
|
||||||
|
result.as_str(),
|
||||||
|
r#"<!--<[hydration::hydration_suspense_no_flickering::{{closure}}::Suspended]>--><!--<[hydration::hydration_suspense_no_flickering::{{closure}}::SuspendedNumber]>--><!--<[hydration::hydration_suspense_no_flickering::{{closure}}::Number]>--><div>0</div><!--</[hydration::hydration_suspense_no_flickering::{{closure}}::Number]>--><!--</[hydration::hydration_suspense_no_flickering::{{closure}}::SuspendedNumber]>--><!--<[hydration::hydration_suspense_no_flickering::{{closure}}::SuspendedNumber]>--><!--<[hydration::hydration_suspense_no_flickering::{{closure}}::Number]>--><div>1</div><!--</[hydration::hydration_suspense_no_flickering::{{closure}}::Number]>--><!--</[hydration::hydration_suspense_no_flickering::{{closure}}::SuspendedNumber]>--><!--<[hydration::hydration_suspense_no_flickering::{{closure}}::SuspendedNumber]>--><!--<[hydration::hydration_suspense_no_flickering::{{closure}}::Number]>--><div>2</div><!--</[hydration::hydration_suspense_no_flickering::{{closure}}::Number]>--><!--</[hydration::hydration_suspense_no_flickering::{{closure}}::SuspendedNumber]>--><!--<[hydration::hydration_suspense_no_flickering::{{closure}}::SuspendedNumber]>--><!--<[hydration::hydration_suspense_no_flickering::{{closure}}::Number]>--><div>3</div><!--</[hydration::hydration_suspense_no_flickering::{{closure}}::Number]>--><!--</[hydration::hydration_suspense_no_flickering::{{closure}}::SuspendedNumber]>--><!--<[hydration::hydration_suspense_no_flickering::{{closure}}::SuspendedNumber]>--><!--<[hydration::hydration_suspense_no_flickering::{{closure}}::Number]>--><div>4</div><!--</[hydration::hydration_suspense_no_flickering::{{closure}}::Number]>--><!--</[hydration::hydration_suspense_no_flickering::{{closure}}::SuspendedNumber]>--><!--<[hydration::hydration_suspense_no_flickering::{{closure}}::SuspendedNumber]>--><!--<[hydration::hydration_suspense_no_flickering::{{closure}}::Number]>--><div>5</div><!--</[hydration::hydration_suspense_no_flickering::{{closure}}::Number]>--><!--</[hydration::hydration_suspense_no_flickering::{{closure}}::SuspendedNumber]>--><!--<[hydration::hydration_suspense_no_flickering::{{closure}}::SuspendedNumber]>--><!--<[hydration::hydration_suspense_no_flickering::{{closure}}::Number]>--><div>6</div><!--</[hydration::hydration_suspense_no_flickering::{{closure}}::Number]>--><!--</[hydration::hydration_suspense_no_flickering::{{closure}}::SuspendedNumber]>--><!--<[hydration::hydration_suspense_no_flickering::{{closure}}::SuspendedNumber]>--><!--<[hydration::hydration_suspense_no_flickering::{{closure}}::Number]>--><div>7</div><!--</[hydration::hydration_suspense_no_flickering::{{closure}}::Number]>--><!--</[hydration::hydration_suspense_no_flickering::{{closure}}::SuspendedNumber]>--><!--<[hydration::hydration_suspense_no_flickering::{{closure}}::SuspendedNumber]>--><!--<[hydration::hydration_suspense_no_flickering::{{closure}}::Number]>--><div>8</div><!--</[hydration::hydration_suspense_no_flickering::{{closure}}::Number]>--><!--</[hydration::hydration_suspense_no_flickering::{{closure}}::SuspendedNumber]>--><!--<[hydration::hydration_suspense_no_flickering::{{closure}}::SuspendedNumber]>--><!--<[hydration::hydration_suspense_no_flickering::{{closure}}::Number]>--><div>9</div><!--</[hydration::hydration_suspense_no_flickering::{{closure}}::Number]>--><!--</[hydration::hydration_suspense_no_flickering::{{closure}}::SuspendedNumber]>--><!--</[hydration::hydration_suspense_no_flickering::{{closure}}::Suspended]>-->"#
|
||||||
|
);
|
||||||
|
sleep(Duration::from_millis(26)).await;
|
||||||
|
|
||||||
|
let result = obtain_result_by_id("output");
|
||||||
|
assert_eq!(
|
||||||
|
result.as_str(),
|
||||||
|
r#"<!--<[hydration::hydration_suspense_no_flickering::{{closure}}::Suspended]>--><!--<[hydration::hydration_suspense_no_flickering::{{closure}}::SuspendedNumber]>--><!--<[hydration::hydration_suspense_no_flickering::{{closure}}::Number]>--><div>0</div><!--</[hydration::hydration_suspense_no_flickering::{{closure}}::Number]>--><!--</[hydration::hydration_suspense_no_flickering::{{closure}}::SuspendedNumber]>--><!--<[hydration::hydration_suspense_no_flickering::{{closure}}::SuspendedNumber]>--><!--<[hydration::hydration_suspense_no_flickering::{{closure}}::Number]>--><div>1</div><!--</[hydration::hydration_suspense_no_flickering::{{closure}}::Number]>--><!--</[hydration::hydration_suspense_no_flickering::{{closure}}::SuspendedNumber]>--><!--<[hydration::hydration_suspense_no_flickering::{{closure}}::SuspendedNumber]>--><!--<[hydration::hydration_suspense_no_flickering::{{closure}}::Number]>--><div>2</div><!--</[hydration::hydration_suspense_no_flickering::{{closure}}::Number]>--><!--</[hydration::hydration_suspense_no_flickering::{{closure}}::SuspendedNumber]>--><!--<[hydration::hydration_suspense_no_flickering::{{closure}}::SuspendedNumber]>--><!--<[hydration::hydration_suspense_no_flickering::{{closure}}::Number]>--><div>3</div><!--</[hydration::hydration_suspense_no_flickering::{{closure}}::Number]>--><!--</[hydration::hydration_suspense_no_flickering::{{closure}}::SuspendedNumber]>--><!--<[hydration::hydration_suspense_no_flickering::{{closure}}::SuspendedNumber]>--><!--<[hydration::hydration_suspense_no_flickering::{{closure}}::Number]>--><div>4</div><!--</[hydration::hydration_suspense_no_flickering::{{closure}}::Number]>--><!--</[hydration::hydration_suspense_no_flickering::{{closure}}::SuspendedNumber]>--><!--<[hydration::hydration_suspense_no_flickering::{{closure}}::SuspendedNumber]>--><!--<[hydration::hydration_suspense_no_flickering::{{closure}}::Number]>--><div>5</div><!--</[hydration::hydration_suspense_no_flickering::{{closure}}::Number]>--><!--</[hydration::hydration_suspense_no_flickering::{{closure}}::SuspendedNumber]>--><!--<[hydration::hydration_suspense_no_flickering::{{closure}}::SuspendedNumber]>--><!--<[hydration::hydration_suspense_no_flickering::{{closure}}::Number]>--><div>6</div><!--</[hydration::hydration_suspense_no_flickering::{{closure}}::Number]>--><!--</[hydration::hydration_suspense_no_flickering::{{closure}}::SuspendedNumber]>--><!--<[hydration::hydration_suspense_no_flickering::{{closure}}::SuspendedNumber]>--><!--<[hydration::hydration_suspense_no_flickering::{{closure}}::Number]>--><div>7</div><!--</[hydration::hydration_suspense_no_flickering::{{closure}}::Number]>--><!--</[hydration::hydration_suspense_no_flickering::{{closure}}::SuspendedNumber]>--><!--<[hydration::hydration_suspense_no_flickering::{{closure}}::SuspendedNumber]>--><!--<[hydration::hydration_suspense_no_flickering::{{closure}}::Number]>--><div>8</div><!--</[hydration::hydration_suspense_no_flickering::{{closure}}::Number]>--><!--</[hydration::hydration_suspense_no_flickering::{{closure}}::SuspendedNumber]>--><!--<[hydration::hydration_suspense_no_flickering::{{closure}}::SuspendedNumber]>--><!--<[hydration::hydration_suspense_no_flickering::{{closure}}::Number]>--><div>9</div><!--</[hydration::hydration_suspense_no_flickering::{{closure}}::Number]>--><!--</[hydration::hydration_suspense_no_flickering::{{closure}}::SuspendedNumber]>--><!--</[hydration::hydration_suspense_no_flickering::{{closure}}::Suspended]>-->"#
|
||||||
|
);
|
||||||
|
sleep(Duration::from_millis(26)).await;
|
||||||
|
|
||||||
|
let result = obtain_result_by_id("output");
|
||||||
|
assert_eq!(
|
||||||
|
result.as_str(),
|
||||||
|
// outer revealed, inner still suspended, outer remains.
|
||||||
|
r#"<!--<[hydration::hydration_suspense_no_flickering::{{closure}}::Suspended]>--><!--<[hydration::hydration_suspense_no_flickering::{{closure}}::SuspendedNumber]>--><!--<[hydration::hydration_suspense_no_flickering::{{closure}}::Number]>--><div>0</div><!--</[hydration::hydration_suspense_no_flickering::{{closure}}::Number]>--><!--</[hydration::hydration_suspense_no_flickering::{{closure}}::SuspendedNumber]>--><!--<[hydration::hydration_suspense_no_flickering::{{closure}}::SuspendedNumber]>--><!--<[hydration::hydration_suspense_no_flickering::{{closure}}::Number]>--><div>1</div><!--</[hydration::hydration_suspense_no_flickering::{{closure}}::Number]>--><!--</[hydration::hydration_suspense_no_flickering::{{closure}}::SuspendedNumber]>--><!--<[hydration::hydration_suspense_no_flickering::{{closure}}::SuspendedNumber]>--><!--<[hydration::hydration_suspense_no_flickering::{{closure}}::Number]>--><div>2</div><!--</[hydration::hydration_suspense_no_flickering::{{closure}}::Number]>--><!--</[hydration::hydration_suspense_no_flickering::{{closure}}::SuspendedNumber]>--><!--<[hydration::hydration_suspense_no_flickering::{{closure}}::SuspendedNumber]>--><!--<[hydration::hydration_suspense_no_flickering::{{closure}}::Number]>--><div>3</div><!--</[hydration::hydration_suspense_no_flickering::{{closure}}::Number]>--><!--</[hydration::hydration_suspense_no_flickering::{{closure}}::SuspendedNumber]>--><!--<[hydration::hydration_suspense_no_flickering::{{closure}}::SuspendedNumber]>--><!--<[hydration::hydration_suspense_no_flickering::{{closure}}::Number]>--><div>4</div><!--</[hydration::hydration_suspense_no_flickering::{{closure}}::Number]>--><!--</[hydration::hydration_suspense_no_flickering::{{closure}}::SuspendedNumber]>--><!--<[hydration::hydration_suspense_no_flickering::{{closure}}::SuspendedNumber]>--><!--<[hydration::hydration_suspense_no_flickering::{{closure}}::Number]>--><div>5</div><!--</[hydration::hydration_suspense_no_flickering::{{closure}}::Number]>--><!--</[hydration::hydration_suspense_no_flickering::{{closure}}::SuspendedNumber]>--><!--<[hydration::hydration_suspense_no_flickering::{{closure}}::SuspendedNumber]>--><!--<[hydration::hydration_suspense_no_flickering::{{closure}}::Number]>--><div>6</div><!--</[hydration::hydration_suspense_no_flickering::{{closure}}::Number]>--><!--</[hydration::hydration_suspense_no_flickering::{{closure}}::SuspendedNumber]>--><!--<[hydration::hydration_suspense_no_flickering::{{closure}}::SuspendedNumber]>--><!--<[hydration::hydration_suspense_no_flickering::{{closure}}::Number]>--><div>7</div><!--</[hydration::hydration_suspense_no_flickering::{{closure}}::Number]>--><!--</[hydration::hydration_suspense_no_flickering::{{closure}}::SuspendedNumber]>--><!--<[hydration::hydration_suspense_no_flickering::{{closure}}::SuspendedNumber]>--><!--<[hydration::hydration_suspense_no_flickering::{{closure}}::Number]>--><div>8</div><!--</[hydration::hydration_suspense_no_flickering::{{closure}}::Number]>--><!--</[hydration::hydration_suspense_no_flickering::{{closure}}::SuspendedNumber]>--><!--<[hydration::hydration_suspense_no_flickering::{{closure}}::SuspendedNumber]>--><!--<[hydration::hydration_suspense_no_flickering::{{closure}}::Number]>--><div>9</div><!--</[hydration::hydration_suspense_no_flickering::{{closure}}::Number]>--><!--</[hydration::hydration_suspense_no_flickering::{{closure}}::SuspendedNumber]>--><!--</[hydration::hydration_suspense_no_flickering::{{closure}}::Suspended]>-->"#
|
||||||
|
);
|
||||||
|
|
||||||
|
sleep(Duration::from_millis(26)).await;
|
||||||
|
|
||||||
|
let result = obtain_result_by_id("output");
|
||||||
|
assert_eq!(
|
||||||
|
result.as_str(),
|
||||||
|
// inner revealed.
|
||||||
|
r#"<div>0</div><div>1</div><div>2</div><div>3</div><div>4</div><div>5</div><div>6</div><div>7</div><div>8</div><div>9</div>"#
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[wasm_bindgen_test]
|
||||||
|
async fn hydration_order_issue_nested_suspense() {
|
||||||
|
#[function_component(App)]
|
||||||
|
pub fn app() -> Html {
|
||||||
|
let elems = (0..10).map(|number: u32| {
|
||||||
|
html! {
|
||||||
|
<ToSuspendOrNot {number} />
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
html! {
|
||||||
|
<Suspense>
|
||||||
|
{ for elems }
|
||||||
|
</Suspense>
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Properties, PartialEq)]
|
||||||
|
struct NumberProps {
|
||||||
|
number: u32,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[function_component(Number)]
|
||||||
|
fn number(props: &NumberProps) -> Html {
|
||||||
|
html! {
|
||||||
|
<div>{props.number.to_string()}</div>
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[function_component(SuspendedNumber)]
|
||||||
|
fn suspended_number(props: &NumberProps) -> HtmlResult {
|
||||||
|
use_suspend()?;
|
||||||
|
Ok(html! {
|
||||||
|
<div>{props.number.to_string()}</div>
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
#[function_component(ToSuspendOrNot)]
|
||||||
|
fn suspend_or_not(props: &NumberProps) -> HtmlResult {
|
||||||
|
let number = props.number;
|
||||||
|
Ok(html! {
|
||||||
|
if number % 3 == 0 {
|
||||||
|
<Suspense>
|
||||||
|
<SuspendedNumber {number} />
|
||||||
|
</Suspense>
|
||||||
|
} else {
|
||||||
|
<Number {number} />
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
#[hook]
|
||||||
|
pub fn use_suspend() -> SuspensionResult<()> {
|
||||||
|
use_future(|| async {})?;
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
let s = ServerRenderer::<App>::new().render().await;
|
||||||
|
|
||||||
|
gloo::utils::document()
|
||||||
|
.query_selector("#output")
|
||||||
|
.unwrap()
|
||||||
|
.unwrap()
|
||||||
|
.set_inner_html(&s);
|
||||||
|
|
||||||
|
sleep(Duration::ZERO).await;
|
||||||
|
|
||||||
|
Renderer::<App>::with_root(gloo_utils::document().get_element_by_id("output").unwrap())
|
||||||
|
.hydrate();
|
||||||
|
|
||||||
|
// Wait until all suspended components becomes revealed.
|
||||||
|
sleep(Duration::ZERO).await;
|
||||||
|
sleep(Duration::ZERO).await;
|
||||||
|
sleep(Duration::ZERO).await;
|
||||||
|
sleep(Duration::ZERO).await;
|
||||||
|
|
||||||
|
let result = obtain_result_by_id("output");
|
||||||
|
assert_eq!(
|
||||||
|
result.as_str(),
|
||||||
|
// Until all components become revealed, there will be component markers.
|
||||||
|
// As long as there's no component markers all components have become unsuspended.
|
||||||
|
r#"<div>0</div><div>1</div><div>2</div><div>3</div><div>4</div><div>5</div><div>6</div><div>7</div><div>8</div><div>9</div>"#
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|||||||
@ -1,3 +1,5 @@
|
|||||||
|
edition = "2021"
|
||||||
|
|
||||||
format_code_in_doc_comments = true
|
format_code_in_doc_comments = true
|
||||||
wrap_comments = true
|
wrap_comments = true
|
||||||
comment_width = 100 # same as default max_width
|
comment_width = 100 # same as default max_width
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user