mirror of
https://github.com/yewstack/yew.git
synced 2025-12-08 21:26:25 +00:00
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:
parent
b246e0d5ed
commit
b7a599b9e4
@ -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.
|
||||
|
||||
@ -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<DynamicDomSlot>,
|
||||
) -> 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,
|
||||
}
|
||||
}
|
||||
|
||||
@ -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<DynamicDomSlot>,
|
||||
) -> 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);
|
||||
}
|
||||
|
||||
@ -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<DynamicDomSlot>,
|
||||
) -> 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(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -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<DynamicDomSlot>,
|
||||
) -> 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,
|
||||
}
|
||||
}
|
||||
|
||||
@ -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<DynamicDomSlot>,
|
||||
) -> 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();
|
||||
|
||||
@ -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<DynamicDomSlot>,
|
||||
) -> 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,
|
||||
|
||||
@ -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<DynamicDomSlot>,
|
||||
) -> Self::Bundle {
|
||||
let next_sibling = if let Some(m) = fragment.front().cloned() {
|
||||
// better safe than sorry.
|
||||
let create_at = |next_sibling: Option<Node>, 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::<TextNode>();
|
||||
// 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
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -94,8 +94,9 @@ mod feat_hydration {
|
||||
parent: &Element,
|
||||
fragment: &mut Fragment,
|
||||
node: VNode,
|
||||
previous_next_sibling: &mut Option<DynamicDomSlot>,
|
||||
) -> Self {
|
||||
let bundle = node.hydrate(root, parent_scope, parent, fragment);
|
||||
let bundle = node.hydrate(root, parent_scope, parent, fragment, previous_next_sibling);
|
||||
Self(bundle)
|
||||
}
|
||||
}
|
||||
|
||||
@ -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<Node> {
|
||||
#[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) {
|
||||
|
||||
@ -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<DynamicDomSlot>,
|
||||
) -> Self::Bundle;
|
||||
}
|
||||
}
|
||||
|
||||
@ -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<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
|
||||
// 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);
|
||||
|
||||
@ -547,17 +547,17 @@ mod feat_csr {
|
||||
root: BSubtree,
|
||||
parent: Element,
|
||||
slot: DomSlot,
|
||||
internal_ref: DynamicDomSlot,
|
||||
props: Rc<COMP::Properties>,
|
||||
) {
|
||||
) -> 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<COMP::Properties>, 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<COMP::Properties>,
|
||||
) {
|
||||
prev_next_sibling: &mut Option<DynamicDomSlot>,
|
||||
) -> 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
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -65,8 +65,7 @@ pub(crate) trait Mountable {
|
||||
parent_scope: &AnyScope,
|
||||
parent: Element,
|
||||
slot: DomSlot,
|
||||
internal_ref: DynamicDomSlot,
|
||||
) -> Box<dyn Scoped>;
|
||||
) -> (Box<dyn Scoped>, DynamicDomSlot);
|
||||
|
||||
#[cfg(feature = "csr")]
|
||||
fn reuse(self: Box<Self>, 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<dyn Scoped>;
|
||||
prev_next_sibling: &mut Option<DynamicDomSlot>,
|
||||
) -> (Box<dyn Scoped>, DynamicDomSlot);
|
||||
}
|
||||
|
||||
pub(crate) struct PropsWrapper<COMP: BaseComponent> {
|
||||
@ -127,12 +126,11 @@ impl<COMP: BaseComponent> Mountable for PropsWrapper<COMP> {
|
||||
parent_scope: &AnyScope,
|
||||
parent: Element,
|
||||
slot: DomSlot,
|
||||
internal_ref: DynamicDomSlot,
|
||||
) -> Box<dyn Scoped> {
|
||||
) -> (Box<dyn Scoped>, DynamicDomSlot) {
|
||||
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")]
|
||||
@ -165,13 +163,14 @@ impl<COMP: BaseComponent> Mountable for PropsWrapper<COMP> {
|
||||
root: BSubtree,
|
||||
parent_scope: &AnyScope,
|
||||
parent: Element,
|
||||
internal_ref: DynamicDomSlot,
|
||||
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()));
|
||||
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)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@ -1078,6 +1078,55 @@ async fn hydrate_empty() {
|
||||
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]
|
||||
async fn hydration_with_camelcase_svg_elements() {
|
||||
#[function_component]
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user