Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 9 additions & 1 deletion packages/yew/src/html/component/lifecycle.rs
Original file line number Diff line number Diff line change
Expand Up @@ -488,6 +488,8 @@ impl ComponentState {
fn commit_render(&mut self, shared_state: &Shared<Option<ComponentState>>, new_vdom: Html) {
// Currently not suspended, we remove any previous suspension and update
// normally.
#[cfg(feature = "csr")]
let resuming_from_suspension = self.suspension.is_some();
self.resume_existing_suspension();

match self.render_state {
Expand All @@ -509,13 +511,19 @@ impl ComponentState {
let first_render = !self.has_rendered;
self.has_rendered = true;

// When resuming from suspension, the component's DOM nodes live in a
// detached parent. The Suspense ancestor must process the Resume
// message and re-render to move children into the live DOM before
// effects run. Use the `rendered` queue (not `rendered_first`) so
// the scheduler processes the Suspense update and render first.
let schedule_as_first = first_render && !resuming_from_suspension;
scheduler::push_component_rendered(
self.comp_id,
Box::new(RenderedRunner {
state: shared_state.clone(),
first_render,
}),
first_render,
schedule_as_first,
);
}

Expand Down
83 changes: 83 additions & 0 deletions packages/yew/tests/suspense.rs
Original file line number Diff line number Diff line change
Expand Up @@ -816,3 +816,86 @@ async fn test_duplicate_suspension() {
let result = obtain_result();
assert_eq!(result.as_str(), "hello!");
}

// Regression test for https://github.com/yewstack/yew/issues/3780
// use_future causes use_effect to fire before DOM is updated, so
// document.get_element_by_id returns None for elements the component renders.
#[wasm_bindgen_test]
async fn use_effect_can_access_dom_after_use_future_resolves() {
#[derive(Properties, Clone)]
struct ContentProps {
dom_observed: Rc<RefCell<Option<bool>>>,
}

impl PartialEq for ContentProps {
fn eq(&self, _other: &Self) -> bool {
true
}
}

#[component(Content)]
fn content(props: &ContentProps) -> HtmlResult {
use_future(|| async {
sleep(Duration::ZERO).await;
})?;

{
let dom_observed = props.dom_observed.clone();
use_effect_with((), move |_| {
let element = gloo::utils::document().get_element_by_id("foo");
*dom_observed.borrow_mut() = Some(element.is_some());
|| {}
});
}

Ok(html! {
<div id="result">
<div id="foo"></div>
</div>
})
}

#[derive(Properties, Clone)]
struct AppProps {
dom_observed: Rc<RefCell<Option<bool>>>,
}

impl PartialEq for AppProps {
fn eq(&self, _other: &Self) -> bool {
true
}
}

#[component(App)]
fn app(props: &AppProps) -> Html {
html! {
<Suspense fallback={html! {<div>{"loading"}</div>}}>
<Content dom_observed={props.dom_observed.clone()} />
</Suspense>
}
}

let dom_observed: Rc<RefCell<Option<bool>>> = Rc::new(RefCell::new(None));

yew::Renderer::<App>::with_root_and_props(
gloo::utils::document().get_element_by_id("output").unwrap(),
AppProps {
dom_observed: dom_observed.clone(),
},
)
.render();

// After everything settles (suspension resolves, component renders, effects run),
// use_effect should have found the #foo element in the DOM.
//
// Bug (issue #3780): use_effect fires before the DOM is updated when use_future
// is involved, so get_element_by_id("foo") returns None and dom_observed is Some(false).
// Expected: use_effect fires after the DOM is committed, so dom_observed should be
// Some(true).
sleep(Duration::from_millis(50)).await;
assert_eq!(
*dom_observed.borrow(),
Some(true),
"use_effect should see the rendered DOM element after use_future resolves"
);
}
Loading