Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Block props update during hydration #2665

Merged
merged 7 commits into from May 23, 2022
Merged
Show file tree
Hide file tree
Changes from 2 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
95 changes: 63 additions & 32 deletions packages/yew/src/html/component/lifecycle.rs
Expand Up @@ -198,7 +198,7 @@ where
};

if self.context.props != props {
self.context.props = Rc::clone(&props);
self.context.props = props;
self.component.changed(&self.context)
} else {
false
Expand All @@ -221,6 +221,8 @@ pub(crate) struct ComponentState {

#[cfg(feature = "csr")]
has_rendered: bool,
#[cfg(feature = "csr")]
pending_props: Option<Rc<dyn Any>>,

suspension: Option<Suspension>,

Expand Down Expand Up @@ -263,6 +265,8 @@ impl ComponentState {

#[cfg(feature = "csr")]
has_rendered: false,
#[cfg(feature = "csr")]
pending_props: None,

comp_id,
}
Expand Down Expand Up @@ -305,7 +309,7 @@ impl<COMP: BaseComponent> Runnable for CreateRunner<COMP> {
pub(crate) struct PropsUpdateRunner {
pub props: Rc<dyn Any>,
pub state: Shared<Option<ComponentState>>,
pub next_sibling: NodeRef,
pub next_sibling: Option<NodeRef>,
}

#[cfg(feature = "csr")]
Expand All @@ -318,43 +322,62 @@ impl Runnable for PropsUpdateRunner {
} = *self;

if let Some(state) = shared_state.borrow_mut().as_mut() {
let schedule_render = match state.render_state {
#[cfg(feature = "csr")]
ComponentRenderState::Render {
next_sibling: ref mut current_next_sibling,
..
} => {
// When components are updated, their siblings were likely also updated
*current_next_sibling = next_sibling;
// Only trigger changed if props were changed
state.inner.props_changed(props)
let next_sibling_changed = match next_sibling {
Some(next_sibling) => {
match state.render_state {
#[cfg(feature = "csr")]
ComponentRenderState::Render {
next_sibling: ref mut current_next_sibling,
..
} => {
let changed = current_next_sibling.get() != next_sibling.get();
// When components are updated, their siblings were likely also updated
*current_next_sibling = next_sibling;

changed
}

#[cfg(feature = "hydration")]
ComponentRenderState::Hydration {
next_sibling: ref mut current_next_sibling,
..
} => {
let changed = current_next_sibling.get() != next_sibling.get();

// When components are updated, their siblings were likely also updated
*current_next_sibling = next_sibling;

changed
}

#[cfg(feature = "ssr")]
ComponentRenderState::Ssr { .. } => {
#[cfg(debug_assertions)]
panic!("properties do not change during SSR");

#[cfg(not(debug_assertions))]
false
}
}
}
None => false,
};

#[cfg(feature = "hydration")]
ComponentRenderState::Hydration {
next_sibling: ref mut current_next_sibling,
..
} => {
// When components are updated, their siblings were likely also updated
*current_next_sibling = next_sibling;
// Only trigger changed if props were changed
state.inner.props_changed(props)
}

#[cfg(feature = "ssr")]
ComponentRenderState::Ssr { .. } => {
#[cfg(debug_assertions)]
panic!("properties do not change during SSR");

#[cfg(not(debug_assertions))]
false
}
// Only trigger changed if props were changed / next sibling has changed.
let schedule_render = if state.has_rendered {
state.inner.props_changed(props) || next_sibling_changed
} else {
state.pending_props = Some(props);
next_sibling_changed
};

#[cfg(debug_assertions)]
super::log_event(
state.comp_id,
format!("props_update(schedule_render={})", schedule_render),
format!(
"props_update(has_rendered={} schedule_render={})",
state.has_rendered, schedule_render
),
);

if schedule_render {
Expand Down Expand Up @@ -621,6 +644,14 @@ mod feat_csr {
if state.suspension.is_none() {
state.inner.rendered(self.first_render);
}

if let Some(m) = state.pending_props.take() {
scheduler::push_component_props_update(Box::new(PropsUpdateRunner {
props: m,
state: self.state.clone(),
next_sibling: None,
}));
}
}
}
}
Expand Down
2 changes: 1 addition & 1 deletion packages/yew/src/html/component/scope.rs
Expand Up @@ -423,7 +423,7 @@ mod feat_csr {
) {
scheduler::push_component_props_update(Box::new(PropsUpdateRunner {
state,
next_sibling,
next_sibling: Some(next_sibling),
props,
}));
// Not guaranteed to already have the scheduler started
Expand Down
67 changes: 66 additions & 1 deletion packages/yew/tests/hydration.rs
@@ -1,6 +1,7 @@
#![cfg(feature = "hydration")]
#![cfg(target_arch = "wasm32")]

use std::ops::Range;
use std::rc::Rc;
use std::time::Duration;

Expand Down Expand Up @@ -834,7 +835,7 @@ async fn hydration_order_issue_nested_suspense() {
pub fn app() -> Html {
let elems = (0..10).map(|number: u32| {
html! {
<ToSuspendOrNot {number} />
<ToSuspendOrNot {number} key={number} />
}
});

Expand Down Expand Up @@ -913,3 +914,67 @@ async fn hydration_order_issue_nested_suspense() {
r#"<div>0</div><div>1</div><div>2</div><div>3</div><div>4</div><div>5</div><div>6</div><div>7</div><div>8</div><div>9</div>"#
);
}

#[wasm_bindgen_test]
async fn hydration_props_blocked_until_hydrated() {
#[function_component(App)]
pub fn app() -> Html {
let range = use_state(|| 0u32..2);
{
let range = range.clone();
use_effect_with_deps(
move |_| {
range.set(0..3);
|| ()
},
(),
);
}

html! {
<Suspense>
<ToSuspend range={(*range).clone()}/>
</Suspense>
}
}

#[derive(Properties, PartialEq)]
struct ToSuspendProps {
range: Range<u32>,
}

#[function_component(ToSuspend)]
fn to_suspend(ToSuspendProps { range }: &ToSuspendProps) -> HtmlResult {
use_suspend(100)?;
futursolo marked this conversation as resolved.
Show resolved Hide resolved
Ok(html! {
{ for range.clone().map(|i|
html!{ <div key={i}>{i}</div> }
)}
})
}

#[hook]
pub fn use_suspend(_wait: u64) -> SuspensionResult<()> {
yew::suspense::use_future(|| async move {
#[cfg(target_arch = "wasm32")]
gloo::timers::future::sleep(Duration::from_millis(_wait)).await;
futursolo marked this conversation as resolved.
Show resolved Hide resolved
})?;

Ok(())
}

let s = ServerRenderer::<App>::new().render().await;

gloo::utils::document()
.query_selector("#output")
.unwrap()
.unwrap()
.set_inner_html(&s);

Renderer::<App>::with_root(gloo_utils::document().get_element_by_id("output").unwrap())
futursolo marked this conversation as resolved.
Show resolved Hide resolved
.hydrate();
sleep(Duration::from_millis(150)).await;

let result = obtain_result_by_id("output");
assert_eq!(result.as_str(), r#"<div>0</div><div>1</div><div>2</div>"#);
}