Fix issue with node refs and hydration (#2597)

* Fix issue with node refs and hydration

When a component is contained in a component, the
inner reconciles, which used to replace the NodeRef,
which left a badly linked one in the outer Hydration render state.

Now, keep a stable internal_ref besides the user-passed node_ref.
The internal_ref never gets replaced as long as the BComp lives.
This commit is contained in:
WorldSEnder 2022-04-13 00:31:48 +02:00 committed by GitHub
parent e9b64e0a45
commit 469cc341c3
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
7 changed files with 61 additions and 31 deletions

View File

@ -12,6 +12,11 @@ use web_sys::Element;
pub(super) struct BComp { pub(super) struct BComp {
type_id: TypeId, type_id: TypeId,
scope: Box<dyn Scoped>, scope: Box<dyn Scoped>,
// A internal NodeRef passed around to track this components position. This
// is "stable", i.e. does not change when reconciled.
internal_ref: NodeRef,
// The user-passed NodeRef from VComp. Might change every time we reconcile.
// Gets linked to the internal ref
node_ref: NodeRef, node_ref: NodeRef,
key: Option<Key>, key: Option<Key>,
} }
@ -57,20 +62,23 @@ impl Reconcilable for VComp {
node_ref, node_ref,
key, key,
} = self; } = self;
let internal_ref = NodeRef::default();
node_ref.link(internal_ref.clone());
let scope = mountable.mount( let scope = mountable.mount(
root, root,
node_ref.clone(), internal_ref.clone(),
parent_scope, parent_scope,
parent.to_owned(), parent.to_owned(),
next_sibling, next_sibling,
); );
( (
node_ref.clone(), internal_ref.clone(),
BComp { BComp {
type_id, type_id,
node_ref, node_ref,
internal_ref,
key, key,
scope, scope,
}, },
@ -112,10 +120,10 @@ impl Reconcilable for VComp {
} = self; } = self;
bcomp.key = key; bcomp.key = key;
let old_ref = std::mem::replace(&mut bcomp.node_ref, node_ref.clone()); let old_ref = std::mem::replace(&mut bcomp.node_ref, node_ref);
bcomp.node_ref.reuse(old_ref); bcomp.node_ref.reuse(old_ref);
mountable.reuse(node_ref.clone(), bcomp.scope.borrow(), next_sibling); mountable.reuse(bcomp.scope.borrow(), next_sibling);
node_ref bcomp.internal_ref.clone()
} }
} }
@ -139,21 +147,24 @@ mod feat_hydration {
node_ref, node_ref,
key, key,
} = self; } = self;
let internal_ref = NodeRef::default();
node_ref.link(internal_ref.clone());
let scoped = mountable.hydrate( let scoped = mountable.hydrate(
root.clone(), root.clone(),
parent_scope, parent_scope,
parent.clone(), parent.clone(),
fragment, fragment,
node_ref.clone(), internal_ref.clone(),
); );
( (
node_ref.clone(), internal_ref.clone(),
BComp { BComp {
type_id, type_id,
scope: scoped, scope: scoped,
node_ref, node_ref,
internal_ref,
key, key,
}, },
) )
@ -165,7 +176,7 @@ mod feat_hydration {
#[cfg(test)] #[cfg(test)]
mod tests { mod tests {
use super::*; use super::*;
use crate::dom_bundle::{Reconcilable, ReconcileTarget}; use crate::dom_bundle::{Bundle, Reconcilable, ReconcileTarget};
use crate::scheduler; use crate::scheduler;
use crate::{ use crate::{
html, html,
@ -442,6 +453,33 @@ mod tests {
scheduler::start_now(); scheduler::start_now();
assert!(node_ref.get().is_none()); assert!(node_ref.get().is_none());
} }
#[test]
fn reset_ancestors_node_ref() {
let (root, scope, parent) = setup_parent();
let mut bundle = Bundle::new();
let node_ref_a = NodeRef::default();
let node_ref_b = NodeRef::default();
let elem = html! { <Comp ref={node_ref_a.clone()}></Comp> };
let node_a = bundle.reconcile(&root, &scope, &parent, NodeRef::default(), elem);
scheduler::start_now();
let node_a = node_a.get().unwrap();
assert!(node_ref_a.get().is_some(), "node_ref_a should be bound");
let elem = html! { <Comp ref={node_ref_b.clone()}></Comp> };
let node_b = bundle.reconcile(&root, &scope, &parent, NodeRef::default(), elem);
scheduler::start_now();
let node_b = node_b.get().unwrap();
assert_eq!(node_a, node_b, "Comp should have reused the element");
assert!(node_ref_b.get().is_some(), "node_ref_b should be bound");
assert!(
node_ref_a.get().is_none(),
"node_ref_a should have been reset when the element was reused."
);
}
} }
#[cfg(feature = "wasm_test")] #[cfg(feature = "wasm_test")]

View File

@ -474,7 +474,7 @@ mod feat_hydration {
let (child_node_ref, child) = child.hydrate(root, parent_scope, parent, fragment); let (child_node_ref, child) = child.hydrate(root, parent_scope, parent, fragment);
if index == 0 { if index == 0 {
node_ref.reuse(child_node_ref); node_ref.link(child_node_ref);
} }
children.push(child); children.push(child);

View File

@ -5,7 +5,10 @@ pub(super) fn insert_node(node: &Node, parent: &Element, next_sibling: Option<&N
match next_sibling { match next_sibling {
Some(next_sibling) => parent Some(next_sibling) => parent
.insert_before(node, Some(next_sibling)) .insert_before(node, Some(next_sibling))
.expect("failed to insert tag before next sibling"), .unwrap_or_else(|err| {
gloo::console::error!("failed to insert node", err, parent, next_sibling, node);
panic!("failed to insert tag before next sibling")
}),
None => parent.append_child(node).expect("failed to append child"), None => parent.append_child(node).expect("failed to append child"),
}; };
} }

View File

@ -305,7 +305,7 @@ pub(crate) enum UpdateEvent {
Message, Message,
/// Wraps properties, node ref, and next sibling for a component /// Wraps properties, node ref, and next sibling for a component
#[cfg(feature = "csr")] #[cfg(feature = "csr")]
Properties(Rc<dyn Any>, NodeRef, NodeRef), Properties(Rc<dyn Any>, NodeRef),
} }
pub(crate) struct UpdateRunner { pub(crate) struct UpdateRunner {
@ -320,16 +320,13 @@ impl Runnable for UpdateRunner {
UpdateEvent::Message => state.inner.flush_messages(), UpdateEvent::Message => state.inner.flush_messages(),
#[cfg(feature = "csr")] #[cfg(feature = "csr")]
UpdateEvent::Properties(props, next_node_ref, next_sibling) => { UpdateEvent::Properties(props, next_sibling) => {
match state.render_state { match state.render_state {
#[cfg(feature = "csr")] #[cfg(feature = "csr")]
ComponentRenderState::Render { ComponentRenderState::Render {
ref mut node_ref,
next_sibling: ref mut current_next_sibling, next_sibling: ref mut current_next_sibling,
.. ..
} => { } => {
// When components are updated, a new node ref could have been passed in
*node_ref = next_node_ref;
// When components are updated, their siblings were likely also updated // When components are updated, their siblings were likely also updated
*current_next_sibling = next_sibling; *current_next_sibling = next_sibling;
// Only trigger changed if props were changed // Only trigger changed if props were changed
@ -338,12 +335,9 @@ impl Runnable for UpdateRunner {
#[cfg(feature = "hydration")] #[cfg(feature = "hydration")]
ComponentRenderState::Hydration { ComponentRenderState::Hydration {
ref mut node_ref,
next_sibling: ref mut current_next_sibling, next_sibling: ref mut current_next_sibling,
.. ..
} => { } => {
// When components are updated, a new node ref could have been passed in
*node_ref = next_node_ref;
// When components are updated, their siblings were likely also updated // When components are updated, their siblings were likely also updated
*current_next_sibling = next_sibling; *current_next_sibling = next_sibling;
// Only trigger changed if props were changed // Only trigger changed if props were changed

View File

@ -457,16 +457,11 @@ mod feat_csr {
scheduler::start(); scheduler::start();
} }
pub(crate) fn reuse( pub(crate) fn reuse(&self, props: Rc<COMP::Properties>, next_sibling: NodeRef) {
&self,
props: Rc<COMP::Properties>,
node_ref: NodeRef,
next_sibling: NodeRef,
) {
#[cfg(debug_assertions)] #[cfg(debug_assertions)]
super::super::log_event(self.id, "reuse"); super::super::log_event(self.id, "reuse");
self.push_update(UpdateEvent::Properties(props, node_ref, next_sibling)); self.push_update(UpdateEvent::Properties(props, next_sibling));
} }
} }

View File

@ -146,9 +146,9 @@ mod feat_csr {
} }
let mut this = self.0.borrow_mut(); let mut this = self.0.borrow_mut();
let existing = node_ref.0.borrow(); let mut existing = node_ref.0.borrow_mut();
this.node = existing.node.clone(); this.node = existing.node.take();
this.link = existing.link.clone(); this.link = existing.link.take();
} }
/// Link a downstream `NodeRef` /// Link a downstream `NodeRef`

View File

@ -65,7 +65,7 @@ pub(crate) trait Mountable {
) -> Box<dyn Scoped>; ) -> Box<dyn Scoped>;
#[cfg(feature = "csr")] #[cfg(feature = "csr")]
fn reuse(self: Box<Self>, node_ref: NodeRef, scope: &dyn Scoped, next_sibling: NodeRef); fn reuse(self: Box<Self>, scope: &dyn Scoped, next_sibling: NodeRef);
#[cfg(feature = "ssr")] #[cfg(feature = "ssr")]
fn render_to_string<'a>( fn render_to_string<'a>(
@ -120,9 +120,9 @@ impl<COMP: BaseComponent> Mountable for PropsWrapper<COMP> {
} }
#[cfg(feature = "csr")] #[cfg(feature = "csr")]
fn reuse(self: Box<Self>, node_ref: NodeRef, scope: &dyn Scoped, next_sibling: NodeRef) { fn reuse(self: Box<Self>, scope: &dyn Scoped, next_sibling: NodeRef) {
let scope: Scope<COMP> = scope.to_any().downcast::<COMP>(); let scope: Scope<COMP> = scope.to_any().downcast::<COMP>();
scope.reuse(self.props, node_ref, next_sibling); scope.reuse(self.props, next_sibling);
} }
#[cfg(feature = "ssr")] #[cfg(feature = "ssr")]