diff --git a/packages/yew/src/html/component/lifecycle.rs b/packages/yew/src/html/component/lifecycle.rs index 517c123d341..d65d660c3b3 100644 --- a/packages/yew/src/html/component/lifecycle.rs +++ b/packages/yew/src/html/component/lifecycle.rs @@ -158,6 +158,9 @@ pub(crate) trait Stateful { fn as_any(&self) -> &dyn Any; fn as_any_mut(&mut self) -> &mut dyn Any; + + #[cfg(feature = "hydration")] + fn mode(&self) -> RenderMode; } impl Stateful for CompStateInner @@ -180,6 +183,11 @@ where self.context.link().clone().into() } + #[cfg(feature = "hydration")] + fn mode(&self) -> RenderMode { + self.context.mode + } + fn flush_messages(&mut self) -> bool { self.context .link() @@ -198,7 +206,7 @@ where }; if self.context.props != props { - self.context.props = Rc::clone(&props); + self.context.props = props; self.component.changed(&self.context) } else { false @@ -221,6 +229,8 @@ pub(crate) struct ComponentState { #[cfg(feature = "csr")] has_rendered: bool, + #[cfg(feature = "hydration")] + pending_props: Option>, suspension: Option, @@ -263,6 +273,8 @@ impl ComponentState { #[cfg(feature = "csr")] has_rendered: false, + #[cfg(feature = "hydration")] + pending_props: None, comp_id, } @@ -303,9 +315,9 @@ impl Runnable for CreateRunner { #[cfg(feature = "csr")] pub(crate) struct PropsUpdateRunner { - pub props: Rc, + pub props: Option>, pub state: Shared>, - pub next_sibling: NodeRef, + pub next_sibling: Option, } #[cfg(feature = "csr")] @@ -318,43 +330,86 @@ 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) + if let Some(next_sibling) = next_sibling { + // When components are updated, their siblings were likely also updated + // We also need to shift the bundle so next sibling will be synced to child + // components. + match state.render_state { + #[cfg(feature = "csr")] + ComponentRenderState::Render { + next_sibling: ref mut current_next_sibling, + ref parent, + ref bundle, + .. + } => { + bundle.shift(parent, next_sibling.clone()); + *current_next_sibling = next_sibling; + } + + #[cfg(feature = "hydration")] + ComponentRenderState::Hydration { + next_sibling: ref mut current_next_sibling, + ref parent, + ref fragment, + .. + } => { + fragment.shift(parent, next_sibling.clone()); + *current_next_sibling = next_sibling; + } + + #[cfg(feature = "ssr")] + ComponentRenderState::Ssr { .. } => { + #[cfg(debug_assertions)] + panic!("properties do not change during SSR"); + } } + } - #[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) - } + let should_render = |props: Option>, state: &mut ComponentState| -> bool { + props.map(|m| state.inner.props_changed(m)).unwrap_or(false) + }; - #[cfg(feature = "ssr")] - ComponentRenderState::Ssr { .. } => { - #[cfg(debug_assertions)] - panic!("properties do not change during SSR"); + #[cfg(feature = "hydration")] + let should_render_hydration = + |props: Option>, state: &mut ComponentState| -> bool { + if let Some(props) = props.or_else(|| state.pending_props.take()) { + match state.has_rendered { + true => { + state.pending_props = None; + state.inner.props_changed(props) + } + false => { + state.pending_props = Some(props); + false + } + } + } else { + false + } + }; - #[cfg(not(debug_assertions))] - false + // Only trigger changed if props were changed / next sibling has changed. + let schedule_render = { + #[cfg(feature = "hydration")] + { + if state.inner.mode() == RenderMode::Hydration { + should_render_hydration(props, state) + } else { + should_render(props, state) + } } + + #[cfg(not(feature = "hydration"))] + should_render(props, state) }; #[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 { @@ -621,6 +676,15 @@ mod feat_csr { if state.suspension.is_none() { state.inner.rendered(self.first_render); } + + #[cfg(feature = "hydration")] + if state.pending_props.is_some() { + scheduler::push_component_props_update(Box::new(PropsUpdateRunner { + props: None, + state: self.state.clone(), + next_sibling: None, + })); + } } } } diff --git a/packages/yew/src/html/component/mod.rs b/packages/yew/src/html/component/mod.rs index edd1809181d..0e3bc9e40af 100644 --- a/packages/yew/src/html/component/mod.rs +++ b/packages/yew/src/html/component/mod.rs @@ -97,6 +97,15 @@ impl Context { /// /// We provide a blanket implementation of this trait for every member that implements /// [`Component`]. +/// +/// # Warning +/// +/// This trait may be subject to heavy changes between versions and is not intended for direct +/// implementation. +/// +/// You should used the [`Component`] trait or the +/// [`#[function_component]`](crate::functional::function_component) macro to define your +/// components. pub trait BaseComponent: Sized + 'static { /// The Component's Message. type Message: 'static; diff --git a/packages/yew/src/html/component/scope.rs b/packages/yew/src/html/component/scope.rs index dc579b9c219..179052ca61b 100644 --- a/packages/yew/src/html/component/scope.rs +++ b/packages/yew/src/html/component/scope.rs @@ -423,8 +423,8 @@ mod feat_csr { ) { scheduler::push_component_props_update(Box::new(PropsUpdateRunner { state, - next_sibling, - props, + next_sibling: Some(next_sibling), + props: Some(props), })); // Not guaranteed to already have the scheduler started scheduler::start(); diff --git a/packages/yew/tests/hydration.rs b/packages/yew/tests/hydration.rs index b0d249015fe..f09ac852fe3 100644 --- a/packages/yew/tests/hydration.rs +++ b/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; @@ -768,7 +769,7 @@ async fn hydration_suspense_no_flickering() { #[hook] pub fn use_suspend() -> SuspensionResult<()> { use_future(|| async { - gloo::timers::future::sleep(std::time::Duration::from_millis(50)).await; + gloo::timers::future::sleep(std::time::Duration::from_millis(200)).await; })?; Ok(()) } @@ -795,21 +796,21 @@ async fn hydration_suspense_no_flickering() { // outer still suspended. r#"
0
1
2
3
4
5
6
7
8
9
"# ); - sleep(Duration::from_millis(26)).await; + sleep(Duration::from_millis(103)).await; let result = obtain_result_by_id("output"); assert_eq!( result.as_str(), r#"
0
1
2
3
4
5
6
7
8
9
"# ); - sleep(Duration::from_millis(26)).await; + sleep(Duration::from_millis(103)).await; let result = obtain_result_by_id("output"); assert_eq!( result.as_str(), r#"
0
1
2
3
4
5
6
7
8
9
"# ); - sleep(Duration::from_millis(26)).await; + sleep(Duration::from_millis(103)).await; let result = obtain_result_by_id("output"); assert_eq!( @@ -818,7 +819,7 @@ async fn hydration_suspense_no_flickering() { r#"
0
1
2
3
4
5
6
7
8
9
"# ); - sleep(Duration::from_millis(26)).await; + sleep(Duration::from_millis(103)).await; let result = obtain_result_by_id("output"); assert_eq!( @@ -834,7 +835,7 @@ async fn hydration_order_issue_nested_suspense() { pub fn app() -> Html { let elems = (0..10).map(|number: u32| { html! { - + } }); @@ -913,3 +914,66 @@ async fn hydration_order_issue_nested_suspense() { r#"
0
1
2
3
4
5
6
7
8
9
"# ); } + +#[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! { + + + + } + } + + #[derive(Properties, PartialEq)] + struct ToSuspendProps { + range: Range, + } + + #[function_component(ToSuspend)] + fn to_suspend(ToSuspendProps { range }: &ToSuspendProps) -> HtmlResult { + use_suspend(Duration::from_millis(100))?; + Ok(html! { + { for range.clone().map(|i| + html!{
{i}
} + )} + }) + } + + #[hook] + pub fn use_suspend(_dur: Duration) -> SuspensionResult<()> { + yew::suspense::use_future(|| async move { + sleep(_dur).await; + })?; + + Ok(()) + } + + let s = ServerRenderer::::new().render().await; + + let output_element = gloo::utils::document() + .query_selector("#output") + .unwrap() + .unwrap(); + + output_element.set_inner_html(&s); + + Renderer::::with_root(output_element).hydrate(); + sleep(Duration::from_millis(150)).await; + + let result = obtain_result_by_id("output"); + assert_eq!(result.as_str(), r#"
0
1
2
"#); +}