Skip to content

Commit 54923c7

Browse files
fix: update sibling_slot immediately on component reuse (#4093)
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 8c23fb0 commit 54923c7

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
@@ -579,6 +579,19 @@ mod feat_csr {
579579
}
580580

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

packages/yew/tests/use_state.rs

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

0 commit comments

Comments
 (0)