mirror of https://github.com/yewstack/yew
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 commit469cc341c3
. * 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 to109fcfaa12
. * Revert "Revert "Merge branch 'hydration-4' into fc-prepared-state"" This reverts commitf1e408958d
. * Redo #2957.
This commit is contained in:
parent
2db4c81ad6
commit
2576372e26
|
@ -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"
|
||||
|
|
|
@ -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()
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -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
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -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),
|
||||
|
|
|
@ -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
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -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),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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()
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -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())
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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.
|
||||
|
|
|
@ -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(),
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -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.
|
||||
|
|
|
@ -90,8 +90,6 @@ mod feat_csr_ssr {
|
|||
return false;
|
||||
}
|
||||
|
||||
m.listen(self.link.callback(Self::Message::Resume));
|
||||
|
||||
self.suspensions.push(m);
|
||||
|
||||
true
|
||||
|
|
|
@ -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>"#
|
||||
);
|
||||
}
|
||||
|
|
|
@ -1,3 +1,5 @@
|
|||
edition = "2021"
|
||||
|
||||
format_code_in_doc_comments = true
|
||||
wrap_comments = true
|
||||
comment_width = 100 # same as default max_width
|
||||
|
|
Loading…
Reference in New Issue