Fix internally tracked sibling positions earlier when hydrating (#3914)

* fix internally tracked sibling positions earlier

previously, the fixup was done in the first "real" render, now this
happens immediately in the render that transitions from
ComponentRenderState::Hydration to ComponentRenderState::Render.

We do so by threading through a reference to the sibling that needs
a fix

* test case for element with early change in render order
   harden test case by using an extra inner component
This commit is contained in:
WorldSEnder 2025-09-09 12:18:01 +02:00 committed by GitHub
parent b246e0d5ed
commit b7a599b9e4
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
15 changed files with 215 additions and 113 deletions

View File

@ -5,7 +5,7 @@ use std::rc::Rc;
use web_sys::Element; use web_sys::Element;
use crate::dom_bundle::{BSubtree, DomSlot, DynamicDomSlot}; use crate::dom_bundle::{BSubtree, DomSlot};
use crate::html::{BaseComponent, Scope, Scoped}; use crate::html::{BaseComponent, Scope, Scoped};
/// An instance of an application. /// An instance of an application.
@ -34,13 +34,9 @@ where
scope: Scope::new(None), scope: Scope::new(None),
}; };
let hosting_root = BSubtree::create_root(&host); let hosting_root = BSubtree::create_root(&host);
app.scope.mount_in_place( let _ = app
hosting_root, .scope
host, .mount_in_place(hosting_root, host, DomSlot::at_end(), props);
DomSlot::at_end(),
DynamicDomSlot::new_debug_trapped(),
props,
);
app app
} }
@ -110,15 +106,17 @@ mod feat_hydration {
let mut fragment = Fragment::collect_children(&host); let mut fragment = Fragment::collect_children(&host);
let hosting_root = BSubtree::create_root(&host); let hosting_root = BSubtree::create_root(&host);
let mut previous_next_sibling = None;
app.scope.hydrate_in_place( app.scope.hydrate_in_place(
hosting_root, hosting_root,
host.clone(), host.clone(),
&mut fragment, &mut fragment,
DynamicDomSlot::new_debug_trapped(),
Rc::clone(&props), Rc::clone(&props),
&mut previous_next_sibling,
); );
#[cfg(debug_assertions)] // Fix trapped next_sibling at the root if let Some(previous_next_sibling) = previous_next_sibling {
app.scope.reuse(props, DomSlot::at_end()); previous_next_sibling.reassign(DomSlot::at_end());
}
// We remove all remaining nodes, this mimics the clear_element behaviour in // We remove all remaining nodes, this mimics the clear_element behaviour in
// mount_with_props. // mount_with_props.

View File

@ -63,23 +63,16 @@ impl Reconcilable for VComp {
key, key,
.. ..
} = self; } = self;
let internal_ref = DynamicDomSlot::new_debug_trapped();
let scope = mountable.mount( let (scope, internal_ref) = mountable.mount(root, parent_scope, parent.to_owned(), slot);
root,
parent_scope,
parent.to_owned(),
slot,
internal_ref.clone(),
);
( (
internal_ref.to_position(), internal_ref.to_position(),
BComp { BComp {
type_id, type_id,
scope,
own_position: internal_ref, own_position: internal_ref,
key, key,
scope,
}, },
) )
} }
@ -131,6 +124,7 @@ mod feat_hydration {
parent_scope: &AnyScope, parent_scope: &AnyScope,
parent: &Element, parent: &Element,
fragment: &mut Fragment, fragment: &mut Fragment,
prev_next_sibling: &mut Option<DynamicDomSlot>,
) -> Self::Bundle { ) -> Self::Bundle {
let VComp { let VComp {
type_id, type_id,
@ -138,20 +132,19 @@ mod feat_hydration {
key, key,
.. ..
} = self; } = self;
let internal_ref = DynamicDomSlot::new_debug_trapped();
let scoped = mountable.hydrate( let (scope, own_slot) = mountable.hydrate(
root.clone(), root.clone(),
parent_scope, parent_scope,
parent.clone(), parent.clone(),
internal_ref.clone(),
fragment, fragment,
prev_next_sibling,
); );
BComp { BComp {
type_id, type_id,
scope: scoped, scope,
own_position: internal_ref, own_position: own_slot,
key, key,
} }
} }

View File

@ -494,7 +494,7 @@ impl Reconcilable for VList {
#[cfg(feature = "hydration")] #[cfg(feature = "hydration")]
mod feat_hydration { mod feat_hydration {
use super::*; use super::*;
use crate::dom_bundle::{Fragment, Hydratable}; use crate::dom_bundle::{DynamicDomSlot, Fragment, Hydratable};
impl Hydratable for VList { impl Hydratable for VList {
fn hydrate( fn hydrate(
@ -503,13 +503,14 @@ mod feat_hydration {
parent_scope: &AnyScope, parent_scope: &AnyScope,
parent: &Element, parent: &Element,
fragment: &mut Fragment, fragment: &mut Fragment,
prev_next_sibling: &mut Option<DynamicDomSlot>,
) -> Self::Bundle { ) -> Self::Bundle {
let (key, fully_keyed, vchildren) = self.split_for_blist(); let (key, fully_keyed, vchildren) = self.split_for_blist();
let mut children = Vec::with_capacity(vchildren.len()); let mut children = Vec::with_capacity(vchildren.len());
for child in vchildren.into_iter() { 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); children.push(child);
} }

View File

@ -267,7 +267,7 @@ impl fmt::Debug for BNode {
#[cfg(feature = "hydration")] #[cfg(feature = "hydration")]
mod feat_hydration { mod feat_hydration {
use super::*; use super::*;
use crate::dom_bundle::{Fragment, Hydratable}; use crate::dom_bundle::{DynamicDomSlot, Fragment, Hydratable};
impl Hydratable for VNode { impl Hydratable for VNode {
fn hydrate( fn hydrate(
@ -276,17 +276,20 @@ mod feat_hydration {
parent_scope: &AnyScope, parent_scope: &AnyScope,
parent: &Element, parent: &Element,
fragment: &mut Fragment, fragment: &mut Fragment,
prev_next_sibling: &mut Option<DynamicDomSlot>,
) -> Self::Bundle { ) -> Self::Bundle {
match self { match self {
VNode::VTag(vtag) => RcExt::unwrap_or_clone(vtag) 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(), .into(),
VNode::VText(vtext) => vtext.hydrate(root, parent_scope, parent, fragment).into(),
VNode::VComp(vcomp) => RcExt::unwrap_or_clone(vcomp) VNode::VComp(vcomp) => RcExt::unwrap_or_clone(vcomp)
.hydrate(root, parent_scope, parent, fragment) .hydrate(root, parent_scope, parent, fragment, prev_next_sibling)
.into(), .into(),
VNode::VList(vlist) => RcExt::unwrap_or_clone(vlist) VNode::VList(vlist) => RcExt::unwrap_or_clone(vlist)
.hydrate(root, parent_scope, parent, fragment) .hydrate(root, parent_scope, parent, fragment, prev_next_sibling)
.into(), .into(),
// You cannot hydrate a VRef. // You cannot hydrate a VRef.
VNode::VRef(_) => { VNode::VRef(_) => {
@ -303,9 +306,11 @@ mod feat_hydration {
) )
} }
VNode::VSuspense(vsuspense) => RcExt::unwrap_or_clone(vsuspense) 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(), .into(),
VNode::VRaw(vraw) => vraw.hydrate(root, parent_scope, parent, fragment).into(),
} }
} }
} }

View File

@ -142,7 +142,7 @@ impl Reconcilable for VRaw {
#[cfg(feature = "hydration")] #[cfg(feature = "hydration")]
mod feat_hydration { mod feat_hydration {
use super::*; use super::*;
use crate::dom_bundle::{Fragment, Hydratable}; use crate::dom_bundle::{DynamicDomSlot, Fragment, Hydratable};
use crate::virtual_dom::Collectable; use crate::virtual_dom::Collectable;
impl Hydratable for VRaw { impl Hydratable for VRaw {
@ -152,15 +152,24 @@ mod feat_hydration {
_parent_scope: &AnyScope, _parent_scope: &AnyScope,
parent: &Element, parent: &Element,
fragment: &mut Fragment, fragment: &mut Fragment,
prev_next_sibling: &mut Option<DynamicDomSlot>,
) -> Self::Bundle { ) -> Self::Bundle {
let collectable = Collectable::Raw; let collectable = Collectable::Raw;
let fallback_fragment = Fragment::collect_between(fragment, &collectable, parent); 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; let Self { html } = self;
BRaw { BRaw {
children_count: fallback_fragment.len(), children_count: fallback_fragment.len(),
reference: fallback_fragment.iter().next().cloned(), reference: first_child,
html, html,
} }
} }

View File

@ -224,7 +224,7 @@ impl Reconcilable for VSuspense {
#[cfg(feature = "hydration")] #[cfg(feature = "hydration")]
mod feat_hydration { mod feat_hydration {
use super::*; use super::*;
use crate::dom_bundle::{Fragment, Hydratable}; use crate::dom_bundle::{DynamicDomSlot, Fragment, Hydratable};
use crate::virtual_dom::Collectable; use crate::virtual_dom::Collectable;
impl Hydratable for VSuspense { impl Hydratable for VSuspense {
@ -234,6 +234,7 @@ mod feat_hydration {
parent_scope: &AnyScope, parent_scope: &AnyScope,
parent: &Element, parent: &Element,
fragment: &mut Fragment, fragment: &mut Fragment,
previous_next_sibling: &mut Option<DynamicDomSlot>,
) -> Self::Bundle { ) -> Self::Bundle {
let detached_parent = document() let detached_parent = document()
.create_element("div") .create_element("div")
@ -250,9 +251,13 @@ mod feat_hydration {
// Even if initially suspended, these children correspond to the first non-suspended // Even if initially suspended, these children correspond to the first non-suspended
// content Refer to VSuspense::render_to_string // content Refer to VSuspense::render_to_string
let children_bundle = let children_bundle = self.children.hydrate(
self.children root,
.hydrate(root, parent_scope, &detached_parent, &mut nodes); parent_scope,
&detached_parent,
&mut nodes,
previous_next_sibling,
);
// We trim all leading text nodes before checking as it's likely these are whitespaces. // We trim all leading text nodes before checking as it's likely these are whitespaces.
nodes.trim_start_text_nodes(); nodes.trim_start_text_nodes();

View File

@ -337,7 +337,7 @@ mod feat_hydration {
use web_sys::Node; use web_sys::Node;
use super::*; 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 { impl Hydratable for VTag {
fn hydrate( fn hydrate(
@ -346,6 +346,7 @@ mod feat_hydration {
parent_scope: &AnyScope, parent_scope: &AnyScope,
_parent: &Element, _parent: &Element,
fragment: &mut Fragment, fragment: &mut Fragment,
prev_next_sibling: &mut Option<DynamicDomSlot>,
) -> Self::Bundle { ) -> Self::Bundle {
let tag_name = self.tag().to_owned(); let tag_name = self.tag().to_owned();
@ -412,7 +413,12 @@ mod feat_hydration {
} }
VTagInner::Other { children, tag } => { VTagInner::Other { children, tag } => {
let mut nodes = Fragment::collect_children(&el); 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(); nodes.trim_start_text_nodes();
@ -423,6 +429,10 @@ mod feat_hydration {
}; };
node_ref.set(Some((*el).clone())); 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 { BTag {
inner, inner,

View File

@ -92,7 +92,7 @@ mod feat_hydration {
use web_sys::Node; use web_sys::Node;
use super::*; use super::*;
use crate::dom_bundle::{Fragment, Hydratable}; use crate::dom_bundle::{DynamicDomSlot, Fragment, Hydratable};
impl Hydratable for VText { impl Hydratable for VText {
fn hydrate( fn hydrate(
@ -101,9 +101,19 @@ mod feat_hydration {
_parent_scope: &AnyScope, _parent_scope: &AnyScope,
parent: &Element, parent: &Element,
fragment: &mut Fragment, fragment: &mut Fragment,
previous_next_sibling: &mut Option<DynamicDomSlot>,
) -> Self::Bundle { ) -> Self::Bundle {
let next_sibling = if let Some(m) = fragment.front().cloned() { let create_at = |next_sibling: Option<Node>, text: AttrValue| {
// better safe than sorry. // 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 { if m.node_type() == Node::TEXT_NODE {
let m = m.unchecked_into::<TextNode>(); let m = m.unchecked_into::<TextNode>();
// pop current node. // pop current node.
@ -117,27 +127,21 @@ mod feat_hydration {
// Please see the next comment for a detailed explanation. // Please see the next comment for a detailed explanation.
m.set_node_value(Some(self.text.as_ref())); m.set_node_value(Some(self.text.as_ref()));
return BText { BText {
text: self.text, text: self.text,
text_node: m, text_node: m,
}; }
} else {
create_at(Some(m), self.text)
} }
Some(m)
} else { } else {
fragment.sibling_at_end().cloned() create_at(fragment.sibling_at_end().cloned(), self.text)
}; };
if let Some(previous_next_sibling) = previous_next_sibling {
// If there are multiple text nodes placed back-to-back in SSR, it may be parsed as a previous_next_sibling.reassign(DomSlot::at(btext.text_node.clone().into()));
// 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,
} }
*previous_next_sibling = None;
btext
} }
} }
} }

View File

@ -94,8 +94,9 @@ mod feat_hydration {
parent: &Element, parent: &Element,
fragment: &mut Fragment, fragment: &mut Fragment,
node: VNode, node: VNode,
previous_next_sibling: &mut Option<DynamicDomSlot>,
) -> Self { ) -> Self {
let bundle = node.hydrate(root, parent_scope, parent, fragment); let bundle = node.hydrate(root, parent_scope, parent, fragment, previous_next_sibling);
Self(bundle) Self(bundle)
} }
} }

View File

@ -54,6 +54,7 @@ mod trap_impl {
static TRAP: Node = gloo::utils::document().create_element("div").unwrap().into(); static TRAP: Node = gloo::utils::document().create_element("div").unwrap().into();
} }
/// Get a "trap" node, or None if compiled without debug_assertions /// Get a "trap" node, or None if compiled without debug_assertions
#[cfg(feature = "hydration")]
pub fn get_trap_node() -> Option<Node> { pub fn get_trap_node() -> Option<Node> {
#[cfg(debug_assertions)] #[cfg(debug_assertions)]
{ {
@ -98,6 +99,7 @@ impl DomSlot {
/// A new "placeholder" [DomSlot] that should not be used to insert nodes /// A new "placeholder" [DomSlot] that should not be used to insert nodes
#[inline] #[inline]
#[cfg(feature = "hydration")]
pub fn new_debug_trapped() -> Self { pub fn new_debug_trapped() -> Self {
Self::create(trap_impl::get_trap_node()) Self::create(trap_impl::get_trap_node())
} }
@ -165,10 +167,19 @@ impl DynamicDomSlot {
} }
} }
#[cfg(feature = "hydration")]
pub fn new_debug_trapped() -> Self { pub fn new_debug_trapped() -> Self {
Self::new(DomSlot::new_debug_trapped()) 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 /// Change the [`DomSlot`] that is targeted. Subsequently, this will behave as if `self` was
/// created from the passed DomSlot in the first place. /// created from the passed DomSlot in the first place.
pub fn reassign(&self, next_position: DomSlot) { pub fn reassign(&self, next_position: DomSlot) {

View File

@ -100,7 +100,7 @@ pub(super) trait Reconcilable {
#[cfg(feature = "hydration")] #[cfg(feature = "hydration")]
mod feat_hydration { mod feat_hydration {
use super::*; use super::*;
use crate::dom_bundle::Fragment; use crate::dom_bundle::{DynamicDomSlot, Fragment};
pub(in crate::dom_bundle) trait Hydratable: Reconcilable { pub(in crate::dom_bundle) trait Hydratable: Reconcilable {
/// hydrates current tree. /// hydrates current tree.
@ -116,6 +116,12 @@ mod feat_hydration {
parent_scope: &AnyScope, parent_scope: &AnyScope,
parent: &Element, parent: &Element,
fragment: &mut Fragment, 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<DynamicDomSlot>,
) -> Self::Bundle; ) -> Self::Bundle;
} }
} }

View File

@ -25,10 +25,13 @@ pub(crate) enum ComponentRenderState {
bundle: Bundle, bundle: Bundle,
root: BSubtree, root: BSubtree,
parent: Element, 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, sibling_slot: DynamicDomSlot,
/// The dom position in front of this component. Adjusted whenever this component /// The dom position in front of this component.
/// re-renders. /// Gets updated whenever this component re-renders and is shared with the bundle in which
/// this component occurs.
own_slot: DynamicDomSlot, own_slot: DynamicDomSlot,
}, },
#[cfg(feature = "hydration")] #[cfg(feature = "hydration")]
@ -106,10 +109,9 @@ impl ComponentRenderState {
sibling_slot, sibling_slot,
.. ..
} => { } => {
bundle.shift(&next_parent, next_slot.clone());
*parent = next_parent; *parent = next_parent;
sibling_slot.reassign(next_slot); sibling_slot.reassign(next_slot);
bundle.shift(parent, sibling_slot.to_position());
} }
#[cfg(feature = "hydration")] #[cfg(feature = "hydration")]
Self::Hydration { Self::Hydration {
@ -118,10 +120,9 @@ impl ComponentRenderState {
sibling_slot, sibling_slot,
.. ..
} => { } => {
fragment.shift(&next_parent, next_slot.clone());
*parent = next_parent; *parent = next_parent;
sibling_slot.reassign(next_slot); sibling_slot.reassign(next_slot);
fragment.shift(parent, sibling_slot.to_position());
} }
#[cfg(feature = "ssr")] #[cfg(feature = "ssr")]
@ -228,6 +229,9 @@ pub(crate) struct ComponentState {
#[cfg(feature = "csr")] #[cfg(feature = "csr")]
has_rendered: bool, 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")] #[cfg(feature = "hydration")]
pending_props: Option<Rc<dyn Any>>, pending_props: Option<Rc<dyn Any>>,
@ -481,7 +485,7 @@ impl ComponentState {
} }
} }
fn commit_render(&mut self, shared_state: &Shared<Option<ComponentState>>, new_root: Html) { fn commit_render(&mut self, shared_state: &Shared<Option<ComponentState>>, new_vdom: Html) {
// Currently not suspended, we remove any previous suspension and update // Currently not suspended, we remove any previous suspension and update
// normally. // normally.
self.resume_existing_suspension(); self.resume_existing_suspension();
@ -499,7 +503,7 @@ impl ComponentState {
let scope = self.inner.any_scope(); let scope = self.inner.any_scope();
let new_node_ref = 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); own_slot.reassign(new_node_ref);
let first_render = !self.has_rendered; let first_render = !self.has_rendered;
@ -523,8 +527,9 @@ impl ComponentState {
ref mut sibling_slot, ref mut sibling_slot,
ref root, ref root,
} => { } => {
// We schedule a "first" render to run immediately after hydration, // We schedule a "first" render to run immediately after hydration.
// to fix NodeRefs (first_node and slot). // Most notably, only this render will trigger the "rendered" callback, hence we
// want to prioritize this.
scheduler::push_component_priority_render( scheduler::push_component_priority_render(
self.comp_id, self.comp_id,
Box::new(RenderRunner { Box::new(RenderRunner {
@ -533,26 +538,25 @@ impl ComponentState {
); );
let scope = self.inner.any_scope(); let scope = self.inner.any_scope();
let bundle = Bundle::hydrate(
// This first node is not guaranteed to be correct here. root,
// As it may be a comment node that is removed afterwards. &scope,
// but we link it anyways. parent,
let bundle = Bundle::hydrate(root, &scope, parent, fragment, new_root); fragment,
new_vdom,
&mut Some(own_slot.clone()),
);
// We trim all text nodes before checking as it's likely these are whitespaces. // We trim all text nodes before checking as it's likely these are whitespaces.
fragment.trim_start_text_nodes(); fragment.trim_start_text_nodes();
assert!(fragment.is_empty(), "expected end of component, found node"); assert!(fragment.is_empty(), "expected end of component, found node");
self.render_state = ComponentRenderState::Render { self.render_state = ComponentRenderState::Render {
root: root.clone(), root: root.clone(),
bundle, bundle,
parent: parent.clone(), parent: parent.clone(),
own_slot: std::mem::replace(own_slot, DynamicDomSlot::new_debug_trapped()), own_slot: own_slot.take(),
sibling_slot: std::mem::replace( sibling_slot: sibling_slot.take(),
sibling_slot,
DynamicDomSlot::new_debug_trapped(),
),
}; };
} }
@ -560,7 +564,7 @@ impl ComponentState {
ComponentRenderState::Ssr { ref mut sender } => { ComponentRenderState::Ssr { ref mut sender } => {
let _ = shared_state; let _ = shared_state;
if let Some(tx) = sender.take() { 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(); let lifecycle = props.lifecycle.clone();
lifecycle.borrow_mut().clear(); lifecycle.borrow_mut().clear();
scope.mount_in_place( let _ = scope.mount_in_place(root, parent, DomSlot::at_end(), Rc::new(props));
root,
parent,
DomSlot::at_end(),
DynamicDomSlot::new_debug_trapped(),
Rc::new(props),
);
crate::scheduler::start_now(); crate::scheduler::start_now();
assert_eq!(&lifecycle.borrow_mut().deref()[..], expected); assert_eq!(&lifecycle.borrow_mut().deref()[..], expected);

View File

@ -547,17 +547,17 @@ mod feat_csr {
root: BSubtree, root: BSubtree,
parent: Element, parent: Element,
slot: DomSlot, slot: DomSlot,
internal_ref: DynamicDomSlot,
props: Rc<COMP::Properties>, props: Rc<COMP::Properties>,
) { ) -> DynamicDomSlot {
let bundle = Bundle::new(); let bundle = Bundle::new();
let sibling_slot = DynamicDomSlot::new(slot); 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 { let state = ComponentRenderState::Render {
bundle, bundle,
root, root,
own_slot: internal_ref, own_slot,
parent, parent,
sibling_slot, sibling_slot,
}; };
@ -577,6 +577,7 @@ mod feat_csr {
); );
// Not guaranteed to already have the scheduler started // Not guaranteed to already have the scheduler started
scheduler::start(); scheduler::start();
shared_slot
} }
pub(crate) fn reuse(&self, props: Rc<COMP::Properties>, slot: DomSlot) { pub(crate) fn reuse(&self, props: Rc<COMP::Properties>, slot: DomSlot) {
@ -653,7 +654,7 @@ mod feat_hydration {
{ {
/// Hydrates the component. /// Hydrates the component.
/// ///
/// Returns a pending NodeRef of the next sibling. /// Returns the position of the hydrated node in DOM.
/// ///
/// # Note /// # Note
/// ///
@ -664,9 +665,9 @@ mod feat_hydration {
root: BSubtree, root: BSubtree,
parent: Element, parent: Element,
fragment: &mut Fragment, fragment: &mut Fragment,
internal_ref: DynamicDomSlot,
props: Rc<COMP::Properties>, props: Rc<COMP::Properties>,
) { prev_next_sibling: &mut Option<DynamicDomSlot>,
) -> DynamicDomSlot {
// This is very helpful to see which component is failing during hydration // This is very helpful to see which component is failing during hydration
// which means this component may not having a stable layout / differs between // which means this component may not having a stable layout / differs between
// client-side and server-side. // client-side and server-side.
@ -693,11 +694,22 @@ mod feat_hydration {
_ => None, _ => 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 { let state = ComponentRenderState::Hydration {
parent, parent,
root, root,
own_slot: internal_ref, own_slot,
sibling_slot: DynamicDomSlot::new_debug_trapped(), sibling_slot,
fragment, fragment,
}; };
@ -716,6 +728,7 @@ mod feat_hydration {
// Not guaranteed to already have the scheduler started // Not guaranteed to already have the scheduler started
scheduler::start(); scheduler::start();
shared_slot
} }
} }
} }

View File

@ -65,8 +65,7 @@ pub(crate) trait Mountable {
parent_scope: &AnyScope, parent_scope: &AnyScope,
parent: Element, parent: Element,
slot: DomSlot, slot: DomSlot,
internal_ref: DynamicDomSlot, ) -> (Box<dyn Scoped>, DynamicDomSlot);
) -> Box<dyn Scoped>;
#[cfg(feature = "csr")] #[cfg(feature = "csr")]
fn reuse(self: Box<Self>, scope: &dyn Scoped, slot: DomSlot); fn reuse(self: Box<Self>, scope: &dyn Scoped, slot: DomSlot);
@ -86,9 +85,9 @@ pub(crate) trait Mountable {
root: BSubtree, root: BSubtree,
parent_scope: &AnyScope, parent_scope: &AnyScope,
parent: Element, parent: Element,
internal_ref: DynamicDomSlot,
fragment: &mut Fragment, fragment: &mut Fragment,
) -> Box<dyn Scoped>; prev_next_sibling: &mut Option<DynamicDomSlot>,
) -> (Box<dyn Scoped>, DynamicDomSlot);
} }
pub(crate) struct PropsWrapper<COMP: BaseComponent> { pub(crate) struct PropsWrapper<COMP: BaseComponent> {
@ -127,12 +126,11 @@ impl<COMP: BaseComponent> Mountable for PropsWrapper<COMP> {
parent_scope: &AnyScope, parent_scope: &AnyScope,
parent: Element, parent: Element,
slot: DomSlot, slot: DomSlot,
internal_ref: DynamicDomSlot, ) -> (Box<dyn Scoped>, DynamicDomSlot) {
) -> Box<dyn Scoped> {
let scope: Scope<COMP> = Scope::new(Some(parent_scope.clone())); let scope: Scope<COMP> = 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")] #[cfg(feature = "csr")]
@ -165,13 +163,14 @@ impl<COMP: BaseComponent> Mountable for PropsWrapper<COMP> {
root: BSubtree, root: BSubtree,
parent_scope: &AnyScope, parent_scope: &AnyScope,
parent: Element, parent: Element,
internal_ref: DynamicDomSlot,
fragment: &mut Fragment, fragment: &mut Fragment,
) -> Box<dyn Scoped> { prev_next_sibling: &mut Option<DynamicDomSlot>,
) -> (Box<dyn Scoped>, DynamicDomSlot) {
let scope: Scope<COMP> = Scope::new(Some(parent_scope.clone())); let scope: Scope<COMP> = 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)
} }
} }

View File

@ -1078,6 +1078,55 @@ async fn hydrate_empty() {
assert_eq!(result.as_str(), r#"<div>after</div><div>after</div>"#); assert_eq!(result.as_str(), r#"<div>after</div><div>after</div>"#);
} }
#[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! { <p>{text.clone()}</p> }
}
#[component]
fn Flickering() -> Html {
let trigger = use_state(|| false);
let is_first = !*trigger;
if is_first {
trigger.set(true);
html! {
<>
<InnerComp key="1" text="1" />
<InnerComp key="2" text="2" />
</>
}
} else {
html! {
<>
<InnerComp key="2" text="2" />
<InnerComp key="1" text="1" />
</>
}
}
}
let s = ServerRenderer::<Flickering>::new().render().await;
let output_element = gloo::utils::document()
.query_selector("#output")
.unwrap()
.unwrap();
output_element.set_inner_html(&s);
Renderer::<Flickering>::with_root(output_element).hydrate();
sleep(Duration::from_millis(50)).await;
let result = obtain_result_by_id("output");
assert_eq!(result.as_str(), r#"<p>2</p><p>1</p>"#);
}
#[wasm_bindgen_test] #[wasm_bindgen_test]
async fn hydration_with_camelcase_svg_elements() { async fn hydration_with_camelcase_svg_elements() {
#[function_component] #[function_component]