From 3c9adc7473619a97a96f4f97695810ee6a6364c7 Mon Sep 17 00:00:00 2001 From: Matt Yan Date: Mon, 30 Mar 2026 19:45:32 +0900 Subject: [PATCH] fix: update sibling_slot immediately on component reuse During parent reconciliation, BComp::reconcile deferred the sibling_slot update to the scheduler via PropsUpdateRunner. The returned own_position chains through sibling_slot, so for components with empty output the stale reference could point to a node already detached earlier in the same reconciliation pass, causing insertBefore to fail with NotFoundError. Reassign sibling_slot synchronously in Scope::reuse before scheduling the props update so the slot chain resolves correctly while the parent is still reconciling. --- packages/yew/src/html/component/scope.rs | 13 ++++++ packages/yew/tests/use_state.rs | 57 ++++++++++++++++++++++++ 2 files changed, 70 insertions(+) diff --git a/packages/yew/src/html/component/scope.rs b/packages/yew/src/html/component/scope.rs index 88ec635377b..5c74c2d4661 100644 --- a/packages/yew/src/html/component/scope.rs +++ b/packages/yew/src/html/component/scope.rs @@ -581,6 +581,19 @@ mod feat_csr { } pub(crate) fn reuse(&self, props: Rc, slot: DomSlot) { + if let Some(state) = self.state.borrow_mut().as_mut() { + match &state.render_state { + ComponentRenderState::Render { sibling_slot, .. } => { + sibling_slot.reassign(slot.clone()); + } + #[cfg(feature = "hydration")] + ComponentRenderState::Hydration { sibling_slot, .. } => { + sibling_slot.reassign(slot.clone()); + } + #[cfg(feature = "ssr")] + ComponentRenderState::Ssr { .. } => {} + } + } schedule_props_update(self.state.clone(), props, slot) } } diff --git a/packages/yew/tests/use_state.rs b/packages/yew/tests/use_state.rs index d6713d75528..ad24c1ed6d1 100644 --- a/packages/yew/tests/use_state.rs +++ b/packages/yew/tests/use_state.rs @@ -413,3 +413,60 @@ async fn use_state_handle_as_prop_triggers_child_rerender_issue_4058() { CHILD_RENDER_COUNT.load(Ordering::Relaxed) ); } + +#[wasm_bindgen_test] +async fn toggle_conditional_with_empty_component_no_crash() { + use wasm_bindgen::JsCast; + use web_sys::HtmlElement; + + #[component] + fn Empty() -> Html { + html! {} + } + + #[component] + fn App() -> Html { + let toggled = use_state(|| false); + + let onclick = { + let toggled = toggled.clone(); + Callback::from(move |_: MouseEvent| { + toggled.set(!*toggled); + }) + }; + + html! { + <> + if *toggled { + + } + + if !*toggled {
{"old"}
} + +
{ if *toggled { "toggled" } else { "initial" } }
+ + } + } + + yew::Renderer::::with_root(gloo::utils::document().get_element_by_id("output").unwrap()) + .render(); + scheduler::flush().await; + + let result = obtain_result(); + assert_eq!(result.as_str(), "initial"); + + gloo::utils::document() + .get_element_by_id("toggle-btn") + .unwrap() + .unchecked_into::() + .click(); + + scheduler::flush().await; + + let result = obtain_result(); + assert_eq!( + result.as_str(), + "toggled", + "Toggling conditional blocks with empty components must not crash (issue #4092)" + ); +}