diff --git a/examples/js_callback/Cargo.toml b/examples/js_callback/Cargo.toml index 0801247f867..9e3bceb0314 100644 --- a/examples/js_callback/Cargo.toml +++ b/examples/js_callback/Cargo.toml @@ -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" diff --git a/packages/yew/src/dom_bundle/bcomp.rs b/packages/yew/src/dom_bundle/bcomp.rs index 1011bd6e48e..b1f4502a306 100644 --- a/packages/yew/src/dom_bundle/bcomp.rs +++ b/packages/yew/src/dom_bundle/bcomp.rs @@ -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() } } diff --git a/packages/yew/src/dom_bundle/blist.rs b/packages/yew/src/dom_bundle/blist.rs index 3d6c406aab3..1e41054c0f1 100644 --- a/packages/yew/src/dom_bundle/blist.rs +++ b/packages/yew/src/dom_bundle/blist.rs @@ -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 } } diff --git a/packages/yew/src/dom_bundle/bnode.rs b/packages/yew/src/dom_bundle/bnode.rs index b5e4ac5de4f..ebcef51509c 100644 --- a/packages/yew/src/dom_bundle/bnode.rs +++ b/packages/yew/src/dom_bundle/bnode.rs @@ -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), diff --git a/packages/yew/src/dom_bundle/bportal.rs b/packages/yew/src/dom_bundle/bportal.rs index c6ff790a6f2..0484f7f0a3e 100644 --- a/packages/yew/src/dom_bundle/bportal.rs +++ b/packages/yew/src/dom_bundle/bportal.rs @@ -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 } } diff --git a/packages/yew/src/dom_bundle/bsuspense.rs b/packages/yew/src/dom_bundle/bsuspense.rs index 0aee86d034c..bd2dc453740 100644 --- a/packages/yew/src/dom_bundle/bsuspense.rs +++ b/packages/yew/src/dom_bundle/bsuspense.rs @@ -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), } } } diff --git a/packages/yew/src/dom_bundle/btag/mod.rs b/packages/yew/src/dom_bundle/btag/mod.rs index 8c41a6e8ba5..a447d4d44f5 100644 --- a/packages/yew/src/dom_bundle/btag/mod.rs +++ b/packages/yew/src/dom_bundle/btag/mod.rs @@ -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() } } diff --git a/packages/yew/src/dom_bundle/btext.rs b/packages/yew/src/dom_bundle/btext.rs index 17a38debbcf..752e49a3170 100644 --- a/packages/yew/src/dom_bundle/btext.rs +++ b/packages/yew/src/dom_bundle/btext.rs @@ -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()) } } diff --git a/packages/yew/src/dom_bundle/fragment.rs b/packages/yew/src/dom_bundle/fragment.rs index 1ce1c1c0773..6055ecab570 100644 --- a/packages/yew/src/dom_bundle/fragment.rs +++ b/packages/yew/src/dom_bundle/fragment.rs @@ -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) } } diff --git a/packages/yew/src/dom_bundle/traits.rs b/packages/yew/src/dom_bundle/traits.rs index 481ba3929d9..10712be2c04 100644 --- a/packages/yew/src/dom_bundle/traits.rs +++ b/packages/yew/src/dom_bundle/traits.rs @@ -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. diff --git a/packages/yew/src/html/component/lifecycle.rs b/packages/yew/src/html/component/lifecycle.rs index c35b62e8ea8..517c123d341 100644 --- a/packages/yew/src/html/component/lifecycle.rs +++ b/packages/yew/src/html/component/lifecycle.rs @@ -301,61 +301,83 @@ impl Runnable for CreateRunner { } } -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, NodeRef), +#[cfg(feature = "csr")] +pub(crate) struct PropsUpdateRunner { + pub props: Rc, + pub state: Shared>, + pub next_sibling: NodeRef, +} + +#[cfg(feature = "csr")] +impl Runnable for PropsUpdateRunner { + fn run(self: Box) { + 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>, - pub event: UpdateEvent, } impl Runnable for UpdateRunner { fn run(self: Box) { 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(), diff --git a/packages/yew/src/html/component/scope.rs b/packages/yew/src/html/component/scope.rs index dd8cba39576..dc579b9c219 100644 --- a/packages/yew/src/html/component/scope.rs +++ b/packages/yew/src/html/component/scope.rs @@ -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>, + props: Rc, + 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 Scope 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) } } diff --git a/packages/yew/src/scheduler.rs b/packages/yew/src/scheduler.rs index 3da8da98ea0..225eaf96bef 100644 --- a/packages/yew/src/scheduler.rs +++ b/packages/yew/src/scheduler.rs @@ -23,6 +23,8 @@ struct Scheduler { // Component queues destroy: Vec>, create: Vec>, + + props_update: Vec>, update: Vec>, /// 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>, render: BTreeMap>, + render_first: BTreeMap>, + #[cfg(feature = "hydration")] + render_priority: BTreeMap>, /// Binary Tree Map to guarantee children rendered are always called before parent calls rendered_first: BTreeMap>, @@ -113,21 +117,26 @@ mod feat_csr { } }); } + + pub(crate) fn push_component_props_update(props_update: Box) { + 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_first_render(component_id: usize, render: Box) { + pub(crate) fn push_component_priority_render(component_id: usize, render: Box) { with(|s| { - s.render_first.insert(component_id, render); + s.render_priority.insert(component_id, render); }); } } -#[cfg(feature = "csr")] -pub(crate) use feat_csr::*; #[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. diff --git a/packages/yew/src/suspense/component.rs b/packages/yew/src/suspense/component.rs index 42ff2ae2138..99fc5eb9899 100644 --- a/packages/yew/src/suspense/component.rs +++ b/packages/yew/src/suspense/component.rs @@ -90,8 +90,6 @@ mod feat_csr_ssr { return false; } - m.listen(self.link.callback(Self::Message::Resume)); - self.suspensions.push(m); true diff --git a/packages/yew/tests/hydration.rs b/packages/yew/tests/hydration.rs index f85c97e74aa..8b06f5ba9b8 100644 --- a/packages/yew/tests/hydration.rs +++ b/packages/yew/tests/hydration.rs @@ -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#"
"# ); } + +#[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! { +
+ +
+ } + } + + #[derive(Properties, PartialEq)] + struct ListProps { + size: u32, + } + + #[function_component(Test1)] + fn test1() -> Html { + html! { + {"test"} + } + } + #[function_component(Test2)] + fn test2() -> Html { + html! { + + } + } + + #[function_component(List)] + fn list(props: &ListProps) -> Html { + let elems = 0..props.size; + + html! { + <> + { for elems.map(|_| + html! { + + } + )} + + } + } + + let s = ServerRenderer::::new().render().await; + + gloo::utils::document() + .query_selector("#output") + .unwrap() + .unwrap() + .set_inner_html(&s); + + sleep(Duration::ZERO).await; + + Renderer::::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#"
testtesttesttest
"# + ); + + gloo_utils::document() + .query_selector("span") + .unwrap() + .unwrap() + .dyn_into::() + .unwrap() + .click(); + + sleep(Duration::ZERO).await; + + let result = obtain_result_by_id("output"); + assert_eq!( + result.as_str(), + r#"
testtesttesttesttesttesttesttesttesttest
"# + ); +} + +#[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! { + + } + )} + + } + } + + #[derive(Properties, PartialEq)] + struct NumberProps { + number: u32, + } + + #[function_component(Number)] + fn number(props: &NumberProps) -> Html { + html! { +
{props.number.to_string()}
+ } + } + #[function_component(SuspendedNumber)] + fn suspended_number(props: &NumberProps) -> HtmlResult { + use_suspend()?; + Ok(html! { +
{props.number.to_string()}
+ }) + } + #[function_component(ToSuspendOrNot)] + fn suspend_or_not(props: &NumberProps) -> Html { + let number = props.number; + html! { + + if number % 3 == 0 { + + } else { + + } + + } + } + + #[hook] + pub fn use_suspend() -> SuspensionResult<()> { + use_future(|| async {})?; + Ok(()) + } + + let s = ServerRenderer::::new().render().await; + + gloo::utils::document() + .query_selector("#output") + .unwrap() + .unwrap() + .set_inner_html(&s); + + sleep(Duration::ZERO).await; + + Renderer::::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#"
0
1
2
3
4
5
6
7
8
9
"# + ); +} + +#[wasm_bindgen_test] +async fn hydration_suspense_no_flickering() { + #[function_component(App)] + pub fn app() -> Html { + let fallback = html! {

{"Loading..."}

}; + html! { + + + + } + } + + #[derive(Properties, PartialEq, Clone)] + struct NumberProps { + number: u32, + } + + #[function_component(SuspendedNumber)] + fn suspended_number(props: &NumberProps) -> HtmlResult { + use_suspend()?; + + Ok(html! { + + }) + } + #[function_component(Number)] + fn number(props: &NumberProps) -> Html { + html! { +
+ {props.number.to_string()} +
+ } + } + + #[function_component(Suspended)] + fn suspended() -> HtmlResult { + use_suspend()?; + + Ok(html! { + { for (0..10).map(|number| + html! { + + } + )} + }) + } + + #[hook] + pub fn use_suspend() -> SuspensionResult<()> { + use_future(|| async { + gloo::timers::future::sleep(std::time::Duration::from_millis(50)).await; + })?; + Ok(()) + } + + let s = ServerRenderer::::new().render().await; + + gloo::utils::document() + .query_selector("#output") + .unwrap() + .unwrap() + .set_inner_html(&s); + + sleep(Duration::ZERO).await; + + Renderer::::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#"
0
1
2
3
4
5
6
7
8
9
"# + ); + sleep(Duration::from_millis(26)).await; + + let result = obtain_result_by_id("output"); + assert_eq!( + result.as_str(), + r#"
0
1
2
3
4
5
6
7
8
9
"# + ); + sleep(Duration::from_millis(26)).await; + + let result = obtain_result_by_id("output"); + assert_eq!( + result.as_str(), + r#"
0
1
2
3
4
5
6
7
8
9
"# + ); + 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#"
0
1
2
3
4
5
6
7
8
9
"# + ); + + sleep(Duration::from_millis(26)).await; + + let result = obtain_result_by_id("output"); + assert_eq!( + result.as_str(), + // inner revealed. + r#"
0
1
2
3
4
5
6
7
8
9
"# + ); +} + +#[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! { + + } + }); + + html! { + + { for elems } + + } + } + + #[derive(Properties, PartialEq)] + struct NumberProps { + number: u32, + } + + #[function_component(Number)] + fn number(props: &NumberProps) -> Html { + html! { +
{props.number.to_string()}
+ } + } + + #[function_component(SuspendedNumber)] + fn suspended_number(props: &NumberProps) -> HtmlResult { + use_suspend()?; + Ok(html! { +
{props.number.to_string()}
+ }) + } + + #[function_component(ToSuspendOrNot)] + fn suspend_or_not(props: &NumberProps) -> HtmlResult { + let number = props.number; + Ok(html! { + if number % 3 == 0 { + + + + } else { + + } + }) + } + + #[hook] + pub fn use_suspend() -> SuspensionResult<()> { + use_future(|| async {})?; + + Ok(()) + } + + let s = ServerRenderer::::new().render().await; + + gloo::utils::document() + .query_selector("#output") + .unwrap() + .unwrap() + .set_inner_html(&s); + + sleep(Duration::ZERO).await; + + Renderer::::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#"
0
1
2
3
4
5
6
7
8
9
"# + ); +} diff --git a/rustfmt.toml b/rustfmt.toml index 473c88723dd..da3980084a9 100644 --- a/rustfmt.toml +++ b/rustfmt.toml @@ -1,3 +1,5 @@ +edition = "2021" + format_code_in_doc_comments = true wrap_comments = true comment_width = 100 # same as default max_width