diff --git a/packages/yew/src/app_handle.rs b/packages/yew/src/app_handle.rs index 577d34165..94356a2c1 100644 --- a/packages/yew/src/app_handle.rs +++ b/packages/yew/src/app_handle.rs @@ -5,7 +5,7 @@ use std::rc::Rc; use web_sys::Element; -use crate::dom_bundle::{BSubtree, DomSlot, DynamicDomSlot}; +use crate::dom_bundle::{BSubtree, DomSlot}; use crate::html::{BaseComponent, Scope, Scoped}; /// An instance of an application. @@ -34,13 +34,9 @@ where scope: Scope::new(None), }; let hosting_root = BSubtree::create_root(&host); - app.scope.mount_in_place( - hosting_root, - host, - DomSlot::at_end(), - DynamicDomSlot::new_debug_trapped(), - props, - ); + let _ = app + .scope + .mount_in_place(hosting_root, host, DomSlot::at_end(), props); app } @@ -110,15 +106,17 @@ mod feat_hydration { let mut fragment = Fragment::collect_children(&host); let hosting_root = BSubtree::create_root(&host); + let mut previous_next_sibling = None; app.scope.hydrate_in_place( hosting_root, host.clone(), &mut fragment, - DynamicDomSlot::new_debug_trapped(), Rc::clone(&props), + &mut previous_next_sibling, ); - #[cfg(debug_assertions)] // Fix trapped next_sibling at the root - app.scope.reuse(props, DomSlot::at_end()); + if let Some(previous_next_sibling) = previous_next_sibling { + previous_next_sibling.reassign(DomSlot::at_end()); + } // We remove all remaining nodes, this mimics the clear_element behaviour in // mount_with_props. diff --git a/packages/yew/src/dom_bundle/bcomp.rs b/packages/yew/src/dom_bundle/bcomp.rs index 8ae9391b5..013ebaa0c 100644 --- a/packages/yew/src/dom_bundle/bcomp.rs +++ b/packages/yew/src/dom_bundle/bcomp.rs @@ -63,23 +63,16 @@ impl Reconcilable for VComp { key, .. } = self; - let internal_ref = DynamicDomSlot::new_debug_trapped(); - let scope = mountable.mount( - root, - parent_scope, - parent.to_owned(), - slot, - internal_ref.clone(), - ); + let (scope, internal_ref) = mountable.mount(root, parent_scope, parent.to_owned(), slot); ( internal_ref.to_position(), BComp { type_id, + scope, own_position: internal_ref, key, - scope, }, ) } @@ -131,6 +124,7 @@ mod feat_hydration { parent_scope: &AnyScope, parent: &Element, fragment: &mut Fragment, + prev_next_sibling: &mut Option, ) -> Self::Bundle { let VComp { type_id, @@ -138,20 +132,19 @@ mod feat_hydration { key, .. } = self; - let internal_ref = DynamicDomSlot::new_debug_trapped(); - let scoped = mountable.hydrate( + let (scope, own_slot) = mountable.hydrate( root.clone(), parent_scope, parent.clone(), - internal_ref.clone(), fragment, + prev_next_sibling, ); BComp { type_id, - scope: scoped, - own_position: internal_ref, + scope, + own_position: own_slot, key, } } diff --git a/packages/yew/src/dom_bundle/blist.rs b/packages/yew/src/dom_bundle/blist.rs index beedfeebb..cdd148178 100644 --- a/packages/yew/src/dom_bundle/blist.rs +++ b/packages/yew/src/dom_bundle/blist.rs @@ -494,7 +494,7 @@ impl Reconcilable for VList { #[cfg(feature = "hydration")] mod feat_hydration { use super::*; - use crate::dom_bundle::{Fragment, Hydratable}; + use crate::dom_bundle::{DynamicDomSlot, Fragment, Hydratable}; impl Hydratable for VList { fn hydrate( @@ -503,13 +503,14 @@ mod feat_hydration { parent_scope: &AnyScope, parent: &Element, fragment: &mut Fragment, + prev_next_sibling: &mut Option, ) -> Self::Bundle { let (key, fully_keyed, vchildren) = self.split_for_blist(); let mut children = Vec::with_capacity(vchildren.len()); for child in vchildren.into_iter() { - let child = child.hydrate(root, parent_scope, parent, fragment); + let child = child.hydrate(root, parent_scope, parent, fragment, prev_next_sibling); children.push(child); } diff --git a/packages/yew/src/dom_bundle/bnode.rs b/packages/yew/src/dom_bundle/bnode.rs index 5fcd6a28d..a4fa88150 100644 --- a/packages/yew/src/dom_bundle/bnode.rs +++ b/packages/yew/src/dom_bundle/bnode.rs @@ -267,7 +267,7 @@ impl fmt::Debug for BNode { #[cfg(feature = "hydration")] mod feat_hydration { use super::*; - use crate::dom_bundle::{Fragment, Hydratable}; + use crate::dom_bundle::{DynamicDomSlot, Fragment, Hydratable}; impl Hydratable for VNode { fn hydrate( @@ -276,17 +276,20 @@ mod feat_hydration { parent_scope: &AnyScope, parent: &Element, fragment: &mut Fragment, + prev_next_sibling: &mut Option, ) -> Self::Bundle { match self { VNode::VTag(vtag) => RcExt::unwrap_or_clone(vtag) - .hydrate(root, parent_scope, parent, fragment) + .hydrate(root, parent_scope, parent, fragment, prev_next_sibling) + .into(), + VNode::VText(vtext) => vtext + .hydrate(root, parent_scope, parent, fragment, prev_next_sibling) .into(), - VNode::VText(vtext) => vtext.hydrate(root, parent_scope, parent, fragment).into(), VNode::VComp(vcomp) => RcExt::unwrap_or_clone(vcomp) - .hydrate(root, parent_scope, parent, fragment) + .hydrate(root, parent_scope, parent, fragment, prev_next_sibling) .into(), VNode::VList(vlist) => RcExt::unwrap_or_clone(vlist) - .hydrate(root, parent_scope, parent, fragment) + .hydrate(root, parent_scope, parent, fragment, prev_next_sibling) .into(), // You cannot hydrate a VRef. VNode::VRef(_) => { @@ -303,9 +306,11 @@ mod feat_hydration { ) } VNode::VSuspense(vsuspense) => RcExt::unwrap_or_clone(vsuspense) - .hydrate(root, parent_scope, parent, fragment) + .hydrate(root, parent_scope, parent, fragment, prev_next_sibling) + .into(), + VNode::VRaw(vraw) => vraw + .hydrate(root, parent_scope, parent, fragment, prev_next_sibling) .into(), - VNode::VRaw(vraw) => vraw.hydrate(root, parent_scope, parent, fragment).into(), } } } diff --git a/packages/yew/src/dom_bundle/braw.rs b/packages/yew/src/dom_bundle/braw.rs index b579606f3..0b7e6f62e 100644 --- a/packages/yew/src/dom_bundle/braw.rs +++ b/packages/yew/src/dom_bundle/braw.rs @@ -142,7 +142,7 @@ impl Reconcilable for VRaw { #[cfg(feature = "hydration")] mod feat_hydration { use super::*; - use crate::dom_bundle::{Fragment, Hydratable}; + use crate::dom_bundle::{DynamicDomSlot, Fragment, Hydratable}; use crate::virtual_dom::Collectable; impl Hydratable for VRaw { @@ -152,15 +152,24 @@ mod feat_hydration { _parent_scope: &AnyScope, parent: &Element, fragment: &mut Fragment, + prev_next_sibling: &mut Option, ) -> Self::Bundle { let collectable = Collectable::Raw; let fallback_fragment = Fragment::collect_between(fragment, &collectable, parent); + let first_child = fallback_fragment.iter().next().cloned(); + + if let (Some(first_child), prev_next_sibling) = (&first_child, prev_next_sibling) { + if let Some(prev_next_sibling) = prev_next_sibling { + prev_next_sibling.reassign(DomSlot::at(first_child.clone())); + } + *prev_next_sibling = None; + } let Self { html } = self; BRaw { children_count: fallback_fragment.len(), - reference: fallback_fragment.iter().next().cloned(), + reference: first_child, html, } } diff --git a/packages/yew/src/dom_bundle/bsuspense.rs b/packages/yew/src/dom_bundle/bsuspense.rs index 38bb3cf7b..2e8b0ba3d 100644 --- a/packages/yew/src/dom_bundle/bsuspense.rs +++ b/packages/yew/src/dom_bundle/bsuspense.rs @@ -224,7 +224,7 @@ impl Reconcilable for VSuspense { #[cfg(feature = "hydration")] mod feat_hydration { use super::*; - use crate::dom_bundle::{Fragment, Hydratable}; + use crate::dom_bundle::{DynamicDomSlot, Fragment, Hydratable}; use crate::virtual_dom::Collectable; impl Hydratable for VSuspense { @@ -234,6 +234,7 @@ mod feat_hydration { parent_scope: &AnyScope, parent: &Element, fragment: &mut Fragment, + previous_next_sibling: &mut Option, ) -> Self::Bundle { let detached_parent = document() .create_element("div") @@ -250,9 +251,13 @@ mod feat_hydration { // Even if initially suspended, these children correspond to the first non-suspended // content Refer to VSuspense::render_to_string - let children_bundle = - self.children - .hydrate(root, parent_scope, &detached_parent, &mut nodes); + let children_bundle = self.children.hydrate( + root, + parent_scope, + &detached_parent, + &mut nodes, + previous_next_sibling, + ); // We trim all leading text nodes before checking as it's likely these are whitespaces. nodes.trim_start_text_nodes(); diff --git a/packages/yew/src/dom_bundle/btag/mod.rs b/packages/yew/src/dom_bundle/btag/mod.rs index 5eeda1fc9..3b4d58ab0 100644 --- a/packages/yew/src/dom_bundle/btag/mod.rs +++ b/packages/yew/src/dom_bundle/btag/mod.rs @@ -337,7 +337,7 @@ mod feat_hydration { use web_sys::Node; use super::*; - use crate::dom_bundle::{node_type_str, Fragment, Hydratable}; + use crate::dom_bundle::{node_type_str, DynamicDomSlot, Fragment, Hydratable}; impl Hydratable for VTag { fn hydrate( @@ -346,6 +346,7 @@ mod feat_hydration { parent_scope: &AnyScope, _parent: &Element, fragment: &mut Fragment, + prev_next_sibling: &mut Option, ) -> Self::Bundle { let tag_name = self.tag().to_owned(); @@ -412,7 +413,12 @@ mod feat_hydration { } VTagInner::Other { children, tag } => { let mut nodes = Fragment::collect_children(&el); - let child_bundle = children.hydrate(root, parent_scope, &el, &mut nodes); + let mut prev_next_child = None; + let child_bundle = + children.hydrate(root, parent_scope, &el, &mut nodes, &mut prev_next_child); + if let Some(prev_next_child) = prev_next_child { + prev_next_child.reassign(DomSlot::at_end()); + } nodes.trim_start_text_nodes(); @@ -423,6 +429,10 @@ mod feat_hydration { }; node_ref.set(Some((*el).clone())); + if let Some(prev_next_sibling) = prev_next_sibling { + prev_next_sibling.reassign(DomSlot::at((*el).clone())); + } + *prev_next_sibling = None; BTag { inner, diff --git a/packages/yew/src/dom_bundle/btext.rs b/packages/yew/src/dom_bundle/btext.rs index d8ffbdc4f..684836780 100644 --- a/packages/yew/src/dom_bundle/btext.rs +++ b/packages/yew/src/dom_bundle/btext.rs @@ -92,7 +92,7 @@ mod feat_hydration { use web_sys::Node; use super::*; - use crate::dom_bundle::{Fragment, Hydratable}; + use crate::dom_bundle::{DynamicDomSlot, Fragment, Hydratable}; impl Hydratable for VText { fn hydrate( @@ -101,9 +101,19 @@ mod feat_hydration { _parent_scope: &AnyScope, parent: &Element, fragment: &mut Fragment, + previous_next_sibling: &mut Option, ) -> Self::Bundle { - let next_sibling = if let Some(m) = fragment.front().cloned() { - // better safe than sorry. + let create_at = |next_sibling: Option, text: AttrValue| { + // If there are multiple text nodes placed back-to-back in SSR, it may be parsed as + // a single text node by browser, hence we need to add extra text + // nodes here if the next node is not a text node. Similarly, the + // value of the text node may be a combination of multiple VText + // vnodes. So we always need to override their values. + let text_node = document().create_text_node(text.as_ref()); + DomSlot::create(next_sibling).insert(parent, &text_node); + BText { text, text_node } + }; + let btext = if let Some(m) = fragment.front().cloned() { if m.node_type() == Node::TEXT_NODE { let m = m.unchecked_into::(); // pop current node. @@ -117,27 +127,21 @@ mod feat_hydration { // Please see the next comment for a detailed explanation. m.set_node_value(Some(self.text.as_ref())); - return BText { + BText { text: self.text, text_node: m, - }; + } + } else { + create_at(Some(m), self.text) } - Some(m) } else { - fragment.sibling_at_end().cloned() + create_at(fragment.sibling_at_end().cloned(), self.text) }; - - // If there are multiple text nodes placed back-to-back in SSR, it may be parsed as a - // single text node by browser, hence we need to add extra text nodes here - // if the next node is not a text node. Similarly, the value of the text - // node may be a combination of multiple VText vnodes. So we always need to - // override their values. - let text_node = document().create_text_node(""); - DomSlot::create(next_sibling).insert(parent, &text_node); - BText { - text: "".into(), - text_node, + if let Some(previous_next_sibling) = previous_next_sibling { + previous_next_sibling.reassign(DomSlot::at(btext.text_node.clone().into())); } + *previous_next_sibling = None; + btext } } } diff --git a/packages/yew/src/dom_bundle/mod.rs b/packages/yew/src/dom_bundle/mod.rs index bf4e1b864..4a5efe594 100644 --- a/packages/yew/src/dom_bundle/mod.rs +++ b/packages/yew/src/dom_bundle/mod.rs @@ -94,8 +94,9 @@ mod feat_hydration { parent: &Element, fragment: &mut Fragment, node: VNode, + previous_next_sibling: &mut Option, ) -> Self { - let bundle = node.hydrate(root, parent_scope, parent, fragment); + let bundle = node.hydrate(root, parent_scope, parent, fragment, previous_next_sibling); Self(bundle) } } diff --git a/packages/yew/src/dom_bundle/position.rs b/packages/yew/src/dom_bundle/position.rs index 8add115fb..9bdf1f828 100644 --- a/packages/yew/src/dom_bundle/position.rs +++ b/packages/yew/src/dom_bundle/position.rs @@ -54,6 +54,7 @@ mod trap_impl { static TRAP: Node = gloo::utils::document().create_element("div").unwrap().into(); } /// Get a "trap" node, or None if compiled without debug_assertions + #[cfg(feature = "hydration")] pub fn get_trap_node() -> Option { #[cfg(debug_assertions)] { @@ -98,6 +99,7 @@ impl DomSlot { /// A new "placeholder" [DomSlot] that should not be used to insert nodes #[inline] + #[cfg(feature = "hydration")] pub fn new_debug_trapped() -> Self { Self::create(trap_impl::get_trap_node()) } @@ -165,10 +167,19 @@ impl DynamicDomSlot { } } + #[cfg(feature = "hydration")] pub fn new_debug_trapped() -> Self { Self::new(DomSlot::new_debug_trapped()) } + /// Move out of self, leaving behind a trapped slot. `self` should not be used afterwards. + /// Used during the transition from a hydrating to a rendered component to move state between + /// enum variants. + #[cfg(feature = "hydration")] + pub fn take(&mut self) -> Self { + std::mem::replace(self, Self::new(DomSlot::new_debug_trapped())) + } + /// Change the [`DomSlot`] that is targeted. Subsequently, this will behave as if `self` was /// created from the passed DomSlot in the first place. pub fn reassign(&self, next_position: DomSlot) { diff --git a/packages/yew/src/dom_bundle/traits.rs b/packages/yew/src/dom_bundle/traits.rs index 16a423d7c..629b8604d 100644 --- a/packages/yew/src/dom_bundle/traits.rs +++ b/packages/yew/src/dom_bundle/traits.rs @@ -100,7 +100,7 @@ pub(super) trait Reconcilable { #[cfg(feature = "hydration")] mod feat_hydration { use super::*; - use crate::dom_bundle::Fragment; + use crate::dom_bundle::{DynamicDomSlot, Fragment}; pub(in crate::dom_bundle) trait Hydratable: Reconcilable { /// hydrates current tree. @@ -116,6 +116,12 @@ mod feat_hydration { parent_scope: &AnyScope, parent: &Element, fragment: &mut Fragment, + // We hydrate in document order, but need to know the "next sibling" in each component + // to shift elements. (blame Web API for having `Node.insertBefore` but no + // `Node.insertAfter`) Hence, we pass an optional argument to inform of the + // new hydrated node's position. This should end up assigning the same + // position that would have been returned from `Self::attach` on creation. + prev_next_sibling: &mut Option, ) -> Self::Bundle; } } diff --git a/packages/yew/src/html/component/lifecycle.rs b/packages/yew/src/html/component/lifecycle.rs index f8e46942c..b92626301 100644 --- a/packages/yew/src/html/component/lifecycle.rs +++ b/packages/yew/src/html/component/lifecycle.rs @@ -25,10 +25,13 @@ pub(crate) enum ComponentRenderState { bundle: Bundle, root: BSubtree, parent: Element, - /// The dom position in front of the next sibling + /// The dom position in front of the next sibling. + /// Gets updated when the bundle in which this component occurs gets re-rendered and is + /// shared with the children of this component. sibling_slot: DynamicDomSlot, - /// The dom position in front of this component. Adjusted whenever this component - /// re-renders. + /// The dom position in front of this component. + /// Gets updated whenever this component re-renders and is shared with the bundle in which + /// this component occurs. own_slot: DynamicDomSlot, }, #[cfg(feature = "hydration")] @@ -106,10 +109,9 @@ impl ComponentRenderState { sibling_slot, .. } => { - bundle.shift(&next_parent, next_slot.clone()); - *parent = next_parent; sibling_slot.reassign(next_slot); + bundle.shift(parent, sibling_slot.to_position()); } #[cfg(feature = "hydration")] Self::Hydration { @@ -118,10 +120,9 @@ impl ComponentRenderState { sibling_slot, .. } => { - fragment.shift(&next_parent, next_slot.clone()); - *parent = next_parent; sibling_slot.reassign(next_slot); + fragment.shift(parent, sibling_slot.to_position()); } #[cfg(feature = "ssr")] @@ -228,6 +229,9 @@ pub(crate) struct ComponentState { #[cfg(feature = "csr")] has_rendered: bool, + /// This deals with an edge case. Usually, we want to update props as fast as possible. + /// But, when a component hydrates and suspends, we want to continue using the intially given + /// props. This is prop updates are ignored during SSR, too. #[cfg(feature = "hydration")] pending_props: Option>, @@ -481,7 +485,7 @@ impl ComponentState { } } - fn commit_render(&mut self, shared_state: &Shared>, new_root: Html) { + fn commit_render(&mut self, shared_state: &Shared>, new_vdom: Html) { // Currently not suspended, we remove any previous suspension and update // normally. self.resume_existing_suspension(); @@ -499,7 +503,7 @@ impl ComponentState { let scope = self.inner.any_scope(); let new_node_ref = - bundle.reconcile(root, &scope, parent, sibling_slot.to_position(), new_root); + bundle.reconcile(root, &scope, parent, sibling_slot.to_position(), new_vdom); own_slot.reassign(new_node_ref); let first_render = !self.has_rendered; @@ -523,8 +527,9 @@ impl ComponentState { ref mut sibling_slot, ref root, } => { - // We schedule a "first" render to run immediately after hydration, - // to fix NodeRefs (first_node and slot). + // We schedule a "first" render to run immediately after hydration. + // Most notably, only this render will trigger the "rendered" callback, hence we + // want to prioritize this. scheduler::push_component_priority_render( self.comp_id, Box::new(RenderRunner { @@ -533,26 +538,25 @@ impl ComponentState { ); let scope = self.inner.any_scope(); - - // This first node is not guaranteed to be correct here. - // As it may be a comment node that is removed afterwards. - // but we link it anyways. - let bundle = Bundle::hydrate(root, &scope, parent, fragment, new_root); + let bundle = Bundle::hydrate( + root, + &scope, + parent, + fragment, + new_vdom, + &mut Some(own_slot.clone()), + ); // We trim all text nodes before checking as it's likely these are whitespaces. fragment.trim_start_text_nodes(); - assert!(fragment.is_empty(), "expected end of component, found node"); self.render_state = ComponentRenderState::Render { root: root.clone(), bundle, parent: parent.clone(), - own_slot: std::mem::replace(own_slot, DynamicDomSlot::new_debug_trapped()), - sibling_slot: std::mem::replace( - sibling_slot, - DynamicDomSlot::new_debug_trapped(), - ), + own_slot: own_slot.take(), + sibling_slot: sibling_slot.take(), }; } @@ -560,7 +564,7 @@ impl ComponentState { ComponentRenderState::Ssr { ref mut sender } => { let _ = shared_state; if let Some(tx) = sender.take() { - tx.send(new_root).unwrap(); + tx.send(new_vdom).unwrap(); } } }; @@ -875,13 +879,7 @@ mod tests { let lifecycle = props.lifecycle.clone(); lifecycle.borrow_mut().clear(); - scope.mount_in_place( - root, - parent, - DomSlot::at_end(), - DynamicDomSlot::new_debug_trapped(), - Rc::new(props), - ); + let _ = scope.mount_in_place(root, parent, DomSlot::at_end(), Rc::new(props)); crate::scheduler::start_now(); assert_eq!(&lifecycle.borrow_mut().deref()[..], expected); diff --git a/packages/yew/src/html/component/scope.rs b/packages/yew/src/html/component/scope.rs index 952978e94..8a81c5f18 100644 --- a/packages/yew/src/html/component/scope.rs +++ b/packages/yew/src/html/component/scope.rs @@ -547,17 +547,17 @@ mod feat_csr { root: BSubtree, parent: Element, slot: DomSlot, - internal_ref: DynamicDomSlot, props: Rc, - ) { + ) -> DynamicDomSlot { let bundle = Bundle::new(); let sibling_slot = DynamicDomSlot::new(slot); - internal_ref.reassign(sibling_slot.to_position()); + let own_slot = DynamicDomSlot::new(sibling_slot.to_position()); + let shared_slot = own_slot.clone(); let state = ComponentRenderState::Render { bundle, root, - own_slot: internal_ref, + own_slot, parent, sibling_slot, }; @@ -577,6 +577,7 @@ mod feat_csr { ); // Not guaranteed to already have the scheduler started scheduler::start(); + shared_slot } pub(crate) fn reuse(&self, props: Rc, slot: DomSlot) { @@ -653,7 +654,7 @@ mod feat_hydration { { /// Hydrates the component. /// - /// Returns a pending NodeRef of the next sibling. + /// Returns the position of the hydrated node in DOM. /// /// # Note /// @@ -664,9 +665,9 @@ mod feat_hydration { root: BSubtree, parent: Element, fragment: &mut Fragment, - internal_ref: DynamicDomSlot, props: Rc, - ) { + prev_next_sibling: &mut Option, + ) -> DynamicDomSlot { // This is very helpful to see which component is failing during hydration // which means this component may not having a stable layout / differs between // client-side and server-side. @@ -693,11 +694,22 @@ mod feat_hydration { _ => None, }; + // These two references need to be fixed before the component is used. + // Our own slot gets fixed on the first render. + // The sibling slot gets shared with the caller to fix up when continuing through + // existing DOM. + let own_slot = DynamicDomSlot::new_debug_trapped(); + let shared_slot = own_slot.clone(); + let sibling_slot = DynamicDomSlot::new_debug_trapped(); + if let Some(prev_next_sibling) = prev_next_sibling { + prev_next_sibling.reassign(shared_slot.to_position()); + } + *prev_next_sibling = Some(sibling_slot.clone()); let state = ComponentRenderState::Hydration { parent, root, - own_slot: internal_ref, - sibling_slot: DynamicDomSlot::new_debug_trapped(), + own_slot, + sibling_slot, fragment, }; @@ -716,6 +728,7 @@ mod feat_hydration { // Not guaranteed to already have the scheduler started scheduler::start(); + shared_slot } } } diff --git a/packages/yew/src/virtual_dom/vcomp.rs b/packages/yew/src/virtual_dom/vcomp.rs index b3146fe6a..0dbc9ce74 100644 --- a/packages/yew/src/virtual_dom/vcomp.rs +++ b/packages/yew/src/virtual_dom/vcomp.rs @@ -65,8 +65,7 @@ pub(crate) trait Mountable { parent_scope: &AnyScope, parent: Element, slot: DomSlot, - internal_ref: DynamicDomSlot, - ) -> Box; + ) -> (Box, DynamicDomSlot); #[cfg(feature = "csr")] fn reuse(self: Box, scope: &dyn Scoped, slot: DomSlot); @@ -86,9 +85,9 @@ pub(crate) trait Mountable { root: BSubtree, parent_scope: &AnyScope, parent: Element, - internal_ref: DynamicDomSlot, fragment: &mut Fragment, - ) -> Box; + prev_next_sibling: &mut Option, + ) -> (Box, DynamicDomSlot); } pub(crate) struct PropsWrapper { @@ -127,12 +126,11 @@ impl Mountable for PropsWrapper { parent_scope: &AnyScope, parent: Element, slot: DomSlot, - internal_ref: DynamicDomSlot, - ) -> Box { + ) -> (Box, DynamicDomSlot) { let scope: Scope = Scope::new(Some(parent_scope.clone())); - scope.mount_in_place(root.clone(), parent, slot, internal_ref, self.props); + let own_slot = scope.mount_in_place(root.clone(), parent, slot, self.props); - Box::new(scope) + (Box::new(scope), own_slot) } #[cfg(feature = "csr")] @@ -165,13 +163,14 @@ impl Mountable for PropsWrapper { root: BSubtree, parent_scope: &AnyScope, parent: Element, - internal_ref: DynamicDomSlot, fragment: &mut Fragment, - ) -> Box { + prev_next_sibling: &mut Option, + ) -> (Box, DynamicDomSlot) { let scope: Scope = Scope::new(Some(parent_scope.clone())); - scope.hydrate_in_place(root, parent, fragment, internal_ref, self.props); + let own_slot = + scope.hydrate_in_place(root, parent, fragment, self.props, prev_next_sibling); - Box::new(scope) + (Box::new(scope), own_slot) } } diff --git a/packages/yew/tests/hydration.rs b/packages/yew/tests/hydration.rs index b2f6db3e8..7fe9f06c9 100644 --- a/packages/yew/tests/hydration.rs +++ b/packages/yew/tests/hydration.rs @@ -1078,6 +1078,55 @@ async fn hydrate_empty() { assert_eq!(result.as_str(), r#"
after
after
"#); } +#[wasm_bindgen_test] +async fn hydrate_flicker() { + // This components renders the same on the server and client during the first render, + // but then immediately changes the order of keyed elements in the next render. + // This should not lead to any hydration failures + #[derive(Properties, PartialEq)] + struct InnerCompProps { + text: String, + } + #[component] + fn InnerComp(InnerCompProps { text }: &InnerCompProps) -> Html { + html! {

{text.clone()}

} + } + #[component] + fn Flickering() -> Html { + let trigger = use_state(|| false); + let is_first = !*trigger; + if is_first { + trigger.set(true); + html! { + <> + + + + } + } else { + html! { + <> + + + + } + } + } + let s = ServerRenderer::::new().render().await; + let output_element = gloo::utils::document() + .query_selector("#output") + .unwrap() + .unwrap(); + + output_element.set_inner_html(&s); + + Renderer::::with_root(output_element).hydrate(); + sleep(Duration::from_millis(50)).await; + + let result = obtain_result_by_id("output"); + assert_eq!(result.as_str(), r#"

2

1

"#); +} + #[wasm_bindgen_test] async fn hydration_with_camelcase_svg_elements() { #[function_component]