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 469cc341c3.

* 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 109fcfaa12.

* Revert "Revert "Merge branch 'hydration-4' into fc-prepared-state""

This reverts commit f1e408958d.

* Redo #2957.
This commit is contained in:
Kaede Hoshikawa 2022-04-25 07:35:55 +09:00 committed by GitHub
parent 2db4c81ad6
commit 2576372e26
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
16 changed files with 542 additions and 92 deletions

View File

@ -7,7 +7,7 @@ license = "MIT OR Apache-2.0"
[dependencies]
wasm-bindgen = "0.2"
yew = { path = "../../packages/yew", features = ["csr"] }
yew = { path = "../../packages/yew", features = ["csr", "tokio"] }
wasm-bindgen-futures = "0.4"
js-sys = "0.3"
once_cell = "1"

View File

@ -44,8 +44,10 @@ impl ReconcileTarget for BComp {
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.node_ref.clone()
}
}

View File

@ -380,10 +380,14 @@ impl ReconcileTarget for BList {
}
}
fn shift(&self, next_parent: &Element, next_sibling: NodeRef) {
for node in self.rev_children.iter().rev() {
node.shift(next_parent, next_sibling.clone());
fn shift(&self, next_parent: &Element, next_sibling: NodeRef) -> NodeRef {
let mut next_sibling = next_sibling;
for node in self.rev_children.iter() {
next_sibling = node.shift(next_parent, next_sibling.clone());
}
next_sibling
}
}

View File

@ -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 {
Self::Tag(ref vtag) => vtag.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
.insert_before(node, next_sibling.get().as_ref())
.unwrap();
NodeRef::new(node.clone())
}
Self::Portal(ref vportal) => vportal.shift(next_parent, next_sibling),
Self::Suspense(ref vsuspense) => vsuspense.shift(next_parent, next_sibling),

View File

@ -26,8 +26,10 @@ impl ReconcileTarget for BPortal {
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.
next_sibling
}
}

View File

@ -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() {
Some(Fallback::Bundle(bundle)) => {
bundle.shift(next_parent, next_sibling);
}
Some(Fallback::Bundle(bundle)) => bundle.shift(next_parent, next_sibling),
#[cfg(feature = "hydration")]
Some(Fallback::Fragment(fragment)) => {
fragment.shift(next_parent, next_sibling);
}
None => {
self.children_bundle.shift(next_parent, next_sibling);
}
Some(Fallback::Fragment(fragment)) => fragment.shift(next_parent, next_sibling),
None => self.children_bundle.shift(next_parent, next_sibling),
}
}
}

View File

@ -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
.insert_before(&self.reference, next_sibling.get().as_ref())
.unwrap();
self.node_ref.clone()
}
}

View File

@ -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;
next_parent
.insert_before(node, next_sibling.get().as_ref())
.unwrap();
NodeRef::new(self.text_node.clone().into())
}
}

View File

@ -156,11 +156,16 @@ impl Fragment {
}
/// 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() {
next_parent
.insert_before(node, next_sibling.get().as_ref())
.unwrap();
}
self.front()
.cloned()
.map(NodeRef::new)
.unwrap_or(next_sibling)
}
}

View File

@ -16,7 +16,7 @@ pub(super) trait ReconcileTarget {
/// Move elements from one parent to another parent.
/// This is for example used by `VSuspense` to preserve component state without detaching
/// (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.

View File

@ -301,61 +301,83 @@ impl<COMP: BaseComponent> Runnable for CreateRunner<COMP> {
}
}
pub(crate) enum UpdateEvent {
/// Drain messages for a component.
Message,
/// Wraps properties, node ref, and next sibling for a component
#[cfg(feature = "csr")]
Properties(Rc<dyn Any>, NodeRef),
#[cfg(feature = "csr")]
pub(crate) struct PropsUpdateRunner {
pub props: Rc<dyn Any>,
pub state: Shared<Option<ComponentState>>,
pub next_sibling: 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 state: Shared<Option<ComponentState>>,
pub event: UpdateEvent,
}
impl Runnable for UpdateRunner {
fn run(self: Box<Self>) {
if let Some(state) = self.state.borrow_mut().as_mut() {
let schedule_render = match self.event {
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
}
}
}
};
let schedule_render = state.inner.flush_messages();
#[cfg(debug_assertions)]
super::log_event(
@ -453,9 +475,8 @@ impl RenderRunner {
if suspension.resumed() {
// schedule a render immediately if suspension is resumed.
scheduler::push_component_render(
state.comp_id,
comp_id,
Box::new(RenderRunner {
state: shared_state,
}),
@ -542,7 +563,7 @@ impl RenderRunner {
} => {
// We schedule a "first" render to run immediately after hydration,
// to fix NodeRefs (first_node and next_sibling).
scheduler::push_component_first_render(
scheduler::push_component_priority_render(
state.comp_id,
Box::new(RenderRunner {
state: self.state.clone(),

View File

@ -9,7 +9,7 @@ use std::rc::Rc;
use std::{fmt, iter};
#[cfg(any(feature = "csr", feature = "ssr"))]
use super::lifecycle::{ComponentState, UpdateEvent, UpdateRunner};
use super::lifecycle::{ComponentState, UpdateRunner};
use super::BaseComponent;
use crate::callback::Callback;
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 {
state: self.state.clone(),
event,
}));
// Not guaranteed to already have the scheduler started
scheduler::start();
@ -369,7 +369,7 @@ mod feat_csr_ssr {
{
// We are the first message in queue, so we queue the update.
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
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 crate::dom_bundle::{BSubtree, Bundle};
use crate::html::component::lifecycle::{
ComponentRenderState, CreateRunner, DestroyRunner, RenderRunner,
ComponentRenderState, CreateRunner, DestroyRunner, PropsUpdateRunner, RenderRunner,
};
use crate::html::NodeRef;
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>
where
COMP: BaseComponent,
@ -459,7 +473,7 @@ mod feat_csr {
#[cfg(debug_assertions)]
super::super::log_event(self.id, "reuse");
self.push_update(UpdateEvent::Properties(props, next_sibling));
schedule_props_update(self.state.clone(), props, next_sibling)
}
}

View File

@ -23,6 +23,8 @@ struct Scheduler {
// Component queues
destroy: Vec<Box<dyn Runnable>>,
create: Vec<Box<dyn Runnable>>,
props_update: Vec<Box<dyn Runnable>>,
update: Vec<Box<dyn Runnable>>,
/// 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
/// rendering parent first.
render_first: 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
rendered_first: BTreeMap<usize, Box<dyn Runnable>>,
@ -113,21 +117,26 @@ mod feat_csr {
}
});
}
}
#[cfg(feature = "hydration")]
mod feat_hydration {
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);
});
pub(crate) fn push_component_props_update(props_update: Box<dyn Runnable>) {
with(|s| s.props_update.push(props_update));
}
}
#[cfg(feature = "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")]
pub(crate) use feat_hydration::*;
@ -226,12 +235,32 @@ impl Scheduler {
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() {
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() {
let rendered_first = std::mem::take(&mut self.rendered_first);
// Children rendered lifecycle happen before parents.

View File

@ -90,8 +90,6 @@ mod feat_csr_ssr {
return false;
}
m.listen(self.link.callback(Self::Message::Resume));
self.suspensions.push(m);
true

View File

@ -12,11 +12,14 @@ use wasm_bindgen_futures::spawn_local;
use wasm_bindgen_test::*;
use web_sys::{HtmlElement, HtmlTextAreaElement};
use yew::prelude::*;
use yew::suspense::{Suspension, SuspensionResult};
use yew::suspense::{use_future, Suspension, SuspensionResult};
use yew::{Renderer, ServerRenderer};
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]
async fn hydration_works() {
#[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>"#
);
}
#[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>"#
);
}

View File

@ -1,3 +1,5 @@
edition = "2021"
format_code_in_doc_comments = true
wrap_comments = true
comment_width = 100 # same as default max_width