Skip to content

Commit 3c9adc7

Browse files
committed
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.
1 parent 83c78a4 commit 3c9adc7

2 files changed

Lines changed: 70 additions & 0 deletions

File tree

packages/yew/src/html/component/scope.rs

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -581,6 +581,19 @@ mod feat_csr {
581581
}
582582

583583
pub(crate) fn reuse(&self, props: Rc<COMP::Properties>, slot: DomSlot) {
584+
if let Some(state) = self.state.borrow_mut().as_mut() {
585+
match &state.render_state {
586+
ComponentRenderState::Render { sibling_slot, .. } => {
587+
sibling_slot.reassign(slot.clone());
588+
}
589+
#[cfg(feature = "hydration")]
590+
ComponentRenderState::Hydration { sibling_slot, .. } => {
591+
sibling_slot.reassign(slot.clone());
592+
}
593+
#[cfg(feature = "ssr")]
594+
ComponentRenderState::Ssr { .. } => {}
595+
}
596+
}
584597
schedule_props_update(self.state.clone(), props, slot)
585598
}
586599
}

packages/yew/tests/use_state.rs

Lines changed: 57 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -413,3 +413,60 @@ async fn use_state_handle_as_prop_triggers_child_rerender_issue_4058() {
413413
CHILD_RENDER_COUNT.load(Ordering::Relaxed)
414414
);
415415
}
416+
417+
#[wasm_bindgen_test]
418+
async fn toggle_conditional_with_empty_component_no_crash() {
419+
use wasm_bindgen::JsCast;
420+
use web_sys::HtmlElement;
421+
422+
#[component]
423+
fn Empty() -> Html {
424+
html! {}
425+
}
426+
427+
#[component]
428+
fn App() -> Html {
429+
let toggled = use_state(|| false);
430+
431+
let onclick = {
432+
let toggled = toggled.clone();
433+
Callback::from(move |_: MouseEvent| {
434+
toggled.set(!*toggled);
435+
})
436+
};
437+
438+
html! {
439+
<>
440+
if *toggled {
441+
<span></span>
442+
}
443+
<Empty />
444+
if !*toggled { <div>{"old"}</div> }
445+
<button id="toggle-btn" {onclick}>{"Toggle"}</button>
446+
<div id="result">{ if *toggled { "toggled" } else { "initial" } }</div>
447+
</>
448+
}
449+
}
450+
451+
yew::Renderer::<App>::with_root(gloo::utils::document().get_element_by_id("output").unwrap())
452+
.render();
453+
scheduler::flush().await;
454+
455+
let result = obtain_result();
456+
assert_eq!(result.as_str(), "initial");
457+
458+
gloo::utils::document()
459+
.get_element_by_id("toggle-btn")
460+
.unwrap()
461+
.unchecked_into::<HtmlElement>()
462+
.click();
463+
464+
scheduler::flush().await;
465+
466+
let result = obtain_result();
467+
assert_eq!(
468+
result.as_str(),
469+
"toggled",
470+
"Toggling conditional blocks with empty components must not crash (issue #4092)"
471+
);
472+
}

0 commit comments

Comments
 (0)