From c98f685b9cfcb4f20b40bc80674ffb8158700310 Mon Sep 17 00:00:00 2001 From: Kaede Hoshikawa Date: Tue, 25 Jan 2022 23:04:59 +0900 Subject: [PATCH 01/40] Make a Renderer. --- packages/yew/src/lib.rs | 74 +------------------- packages/yew/src/renderer.rs | 132 +++++++++++++++++++++++++++++++++++ 2 files changed, 135 insertions(+), 71 deletions(-) create mode 100644 packages/yew/src/renderer.rs diff --git a/packages/yew/src/lib.rs b/packages/yew/src/lib.rs index 6f664495090..19198f79240 100644 --- a/packages/yew/src/lib.rs +++ b/packages/yew/src/lib.rs @@ -84,8 +84,6 @@ #![recursion_limit = "512"] extern crate self as yew; -use std::{cell::Cell, panic::PanicInfo}; - /// This macro provides a convenient way to create [`Classes`]. /// /// The macro takes a list of items similar to the [`vec!`] macro and returns a [`Classes`] instance. @@ -269,6 +267,7 @@ pub mod context; pub mod functional; pub mod html; mod io_coop; +mod renderer; pub mod scheduler; mod sealed; #[cfg(feature = "ssr")] @@ -295,75 +294,8 @@ pub mod events { } pub use crate::app_handle::AppHandle; -use web_sys::Element; - -use crate::html::BaseComponent; - -thread_local! { - static PANIC_HOOK_IS_SET: Cell = Cell::new(false); -} - -/// Set a custom panic hook. -/// Unless a panic hook is set through this function, Yew will -/// overwrite any existing panic hook when one of the `start_app*` functions are called. -pub fn set_custom_panic_hook(hook: Box) + Sync + Send + 'static>) { - std::panic::set_hook(hook); - PANIC_HOOK_IS_SET.with(|hook_is_set| hook_is_set.set(true)); -} - -fn set_default_panic_hook() { - if !PANIC_HOOK_IS_SET.with(|hook_is_set| hook_is_set.replace(true)) { - std::panic::set_hook(Box::new(console_error_panic_hook::hook)); - } -} -/// The main entry point of a Yew application. -/// If you would like to pass props, use the `start_app_with_props_in_element` method. -pub fn start_app_in_element(element: Element) -> AppHandle -where - COMP: BaseComponent, - COMP::Properties: Default, -{ - start_app_with_props_in_element(element, COMP::Properties::default()) -} - -/// Starts an yew app mounted to the body of the document. -/// Alias to start_app_in_element(Body) -pub fn start_app() -> AppHandle -where - COMP: BaseComponent, - COMP::Properties: Default, -{ - start_app_with_props(COMP::Properties::default()) -} - -/// The main entry point of a Yew application. This function does the -/// same as `start_app_in_element(...)` but allows to start an Yew application with properties. -pub fn start_app_with_props_in_element( - element: Element, - props: COMP::Properties, -) -> AppHandle -where - COMP: BaseComponent, -{ - set_default_panic_hook(); - AppHandle::::mount_with_props(element, Rc::new(props)) -} - -/// The main entry point of a Yew application. -/// This function does the same as `start_app(...)` but allows to start an Yew application with properties. -pub fn start_app_with_props(props: COMP::Properties) -> AppHandle -where - COMP: BaseComponent, -{ - start_app_with_props_in_element( - gloo_utils::document() - .body() - .expect("no body node found") - .into(), - props, - ) -} +pub use renderer::*; /// The Yew Prelude /// @@ -386,7 +318,7 @@ pub mod prelude { pub use crate::suspense::Suspense; pub use crate::functional::*; + pub use crate::renderer::*; } pub use self::prelude::*; -use std::rc::Rc; diff --git a/packages/yew/src/renderer.rs b/packages/yew/src/renderer.rs new file mode 100644 index 00000000000..09e4e16ce18 --- /dev/null +++ b/packages/yew/src/renderer.rs @@ -0,0 +1,132 @@ +use std::cell::Cell; +use std::panic::PanicInfo; +use std::rc::Rc; + +use web_sys::Element; + +use crate::app_handle::AppHandle; +use crate::html::BaseComponent; + +thread_local! { + static PANIC_HOOK_IS_SET: Cell = Cell::new(false); +} + +/// Set a custom panic hook. +/// Unless a panic hook is set through this function, Yew will +/// overwrite any existing panic hook when one of the `start_app*` functions are called. +pub fn set_custom_panic_hook(hook: Box) + Sync + Send + 'static>) { + std::panic::set_hook(hook); + PANIC_HOOK_IS_SET.with(|hook_is_set| hook_is_set.set(true)); +} + +fn set_default_panic_hook() { + if !PANIC_HOOK_IS_SET.with(|hook_is_set| hook_is_set.replace(true)) { + std::panic::set_hook(Box::new(console_error_panic_hook::hook)); + } +} + +/// The Yew Renderer. +/// +/// This is the main entry point of a Yew application. +#[derive(Debug)] +pub struct Renderer +where + COMP: BaseComponent + 'static, +{ + root: Element, + props: COMP::Properties, +} + +impl Default for Renderer +where + COMP: BaseComponent + 'static, + COMP::Properties: Default, +{ + fn default() -> Self { + Self::with_props(Default::default()) + } +} + +impl Renderer +where + COMP: BaseComponent + 'static, + COMP::Properties: Default, +{ + /// Creates a [Renderer] that renders into the document body with default properties. + pub fn new() -> Self { + Self::default() + } + + /// Creates a [Renderer] that renders into a custom root with default properties. + pub fn with_root(root: Element) -> Self { + Self::with_root_and_props(root, Default::default()) + } +} + +impl Renderer +where + COMP: BaseComponent + 'static, +{ + /// Creates a [Renderer] that renders into the document body with custom properties. + pub fn with_props(props: COMP::Properties) -> Self { + Self::with_root_and_props( + gloo_utils::document() + .body() + .expect("no body node found") + .into(), + props, + ) + } + + /// Creates a [Renderer] that renders into a custom root with custom properties. + pub fn with_root_and_props(root: Element, props: COMP::Properties) -> Self { + Self { root, props } + } + + /// Renders the application. + pub fn render(self) -> AppHandle { + set_default_panic_hook(); + AppHandle::::mount_with_props(self.root, Rc::new(self.props)) + } +} + +/// The main entry point of a Yew application. +/// If you would like to pass props, use the `start_app_with_props_in_element` method. +pub fn start_app_in_element(element: Element) -> AppHandle +where + COMP: BaseComponent, + COMP::Properties: Default, +{ + Renderer::with_root(element).render() +} + +/// Starts an yew app mounted to the body of the document. +/// Alias to start_app_in_element(Body) +pub fn start_app() -> AppHandle +where + COMP: BaseComponent, + COMP::Properties: Default, +{ + Renderer::new().render() +} + +/// The main entry point of a Yew application. This function does the +/// same as `start_app_in_element(...)` but allows to start an Yew application with properties. +pub fn start_app_with_props_in_element( + element: Element, + props: COMP::Properties, +) -> AppHandle +where + COMP: BaseComponent, +{ + Renderer::with_root_and_props(element, props).render() +} + +/// The main entry point of a Yew application. +/// This function does the same as `start_app(...)` but allows to start an Yew application with properties. +pub fn start_app_with_props(props: COMP::Properties) -> AppHandle +where + COMP: BaseComponent, +{ + Renderer::with_props(props).render() +} From 76b5ecf525d85214bb93c2c16858a32758c54473 Mon Sep 17 00:00:00 2001 From: Kaede Hoshikawa Date: Wed, 26 Jan 2022 21:49:26 +0900 Subject: [PATCH 02/40] Assistive nodes on hydration. --- packages/yew/Cargo.toml | 3 ++- packages/yew/src/html/component/scope.rs | 9 +++++++-- packages/yew/src/renderer.rs | 17 +++++++++++++++++ packages/yew/src/server_renderer.rs | 20 ++++++++++++++++++-- packages/yew/src/virtual_dom/vcomp.rs | 23 ++++++++++++++++++++--- packages/yew/src/virtual_dom/vlist.rs | 9 +++++++-- packages/yew/src/virtual_dom/vnode.rs | 19 ++++++++++++++----- packages/yew/src/virtual_dom/vsuspense.rs | 18 ++++++++++++++++-- packages/yew/src/virtual_dom/vtag.rs | 13 ++++++++++--- packages/yew/src/virtual_dom/vtext.rs | 7 ++++++- 10 files changed, 117 insertions(+), 21 deletions(-) diff --git a/packages/yew/Cargo.toml b/packages/yew/Cargo.toml index 9dee3e2759f..31560ff6688 100644 --- a/packages/yew/Cargo.toml +++ b/packages/yew/Cargo.toml @@ -84,7 +84,8 @@ doc_test = [] wasm_test = [] wasm_bench = [] ssr = ["futures", "html-escape"] -default = [] +hydration = [] +default = ["hydration"] [target.'cfg(not(target_arch = "wasm32"))'.dev-dependencies] tokio = { version = "1.15.0", features = ["full"] } diff --git a/packages/yew/src/html/component/scope.rs b/packages/yew/src/html/component/scope.rs index 426c0e9d5ce..2c2f75697db 100644 --- a/packages/yew/src/html/component/scope.rs +++ b/packages/yew/src/html/component/scope.rs @@ -361,7 +361,12 @@ mod feat_ssr { use futures::channel::oneshot; impl Scope { - pub(crate) async fn render_to_string(&self, w: &mut String, props: Rc) { + pub(crate) async fn render_to_string( + &self, + w: &mut String, + props: Rc, + hydratable: bool, + ) { let (tx, rx) = oneshot::channel(); scheduler::push_component_create( @@ -383,7 +388,7 @@ mod feat_ssr { let html = rx.await.unwrap(); let self_any_scope = self.to_any(); - html.render_to_string(w, &self_any_scope).await; + html.render_to_string(w, &self_any_scope, hydratable).await; scheduler::push_component_destroy(DestroyRunner { state: self.state.clone(), diff --git a/packages/yew/src/renderer.rs b/packages/yew/src/renderer.rs index 09e4e16ce18..09bd76d2089 100644 --- a/packages/yew/src/renderer.rs +++ b/packages/yew/src/renderer.rs @@ -90,6 +90,23 @@ where } } +#[cfg_attr(documenting, doc(cfg(feature = "hydration")))] +#[cfg(feature = "hydration")] +mod feat_hydration { + use super::*; + + impl Renderer + where + COMP: BaseComponent + 'static, + { + /// Hydrates the application. + pub fn hydrate(self) -> AppHandle { + set_default_panic_hook(); + todo!() + } + } +} + /// The main entry point of a Yew application. /// If you would like to pass props, use the `start_app_with_props_in_element` method. pub fn start_app_in_element(element: Element) -> AppHandle diff --git a/packages/yew/src/server_renderer.rs b/packages/yew/src/server_renderer.rs index 9e5cd5fe1cb..69a3fe7f2c2 100644 --- a/packages/yew/src/server_renderer.rs +++ b/packages/yew/src/server_renderer.rs @@ -10,6 +10,7 @@ where COMP: BaseComponent, { props: COMP::Properties, + hydratable: bool, } impl Default for ServerRenderer @@ -39,7 +40,20 @@ where { /// Creates a [ServerRenderer] with custom properties. pub fn with_props(props: COMP::Properties) -> Self { - Self { props } + Self { + props, + hydratable: true, + } + } + + /// Sets whether an the rendered result is hydratable. + /// + /// Defaults to `true`. + /// + /// When this is sets to `true`, the rendered artifact will include assistive nodes + /// to assist with the hydration process. + pub fn set_hydratable(&mut self, val: bool) { + self.hydratable = val; } /// Renders Yew Application. @@ -54,6 +68,8 @@ where /// Renders Yew Application to a String. pub async fn render_to_string(self, w: &mut String) { let scope = Scope::::new(None); - scope.render_to_string(w, self.props.into()).await; + scope + .render_to_string(w, self.props.into(), self.hydratable) + .await; } } diff --git a/packages/yew/src/virtual_dom/vcomp.rs b/packages/yew/src/virtual_dom/vcomp.rs index 32b921f3863..444e0d50f75 100644 --- a/packages/yew/src/virtual_dom/vcomp.rs +++ b/packages/yew/src/virtual_dom/vcomp.rs @@ -189,6 +189,7 @@ trait Mountable { &'a self, w: &'a mut String, parent_scope: &'a AnyScope, + hydratable: bool, ) -> LocalBoxFuture<'a, ()>; } @@ -233,10 +234,13 @@ impl Mountable for PropsWrapper { &'a self, w: &'a mut String, parent_scope: &'a AnyScope, + hydratable: bool, ) -> LocalBoxFuture<'a, ()> { async move { let scope: Scope = Scope::new(Some(parent_scope.clone())); - scope.render_to_string(w, self.props.clone()).await; + scope + .render_to_string(w, self.props.clone(), hydratable) + .await; } .boxed_local() } @@ -313,13 +317,26 @@ mod feat_ssr { use super::*; impl VComp { - pub(crate) async fn render_to_string(&self, w: &mut String, parent_scope: &AnyScope) { + pub(crate) async fn render_to_string( + &self, + w: &mut String, + parent_scope: &AnyScope, + hydratable: bool, + ) { + if hydratable { + w.push_str(""); + } + self.mountable .as_ref() .map(|m| m.copy()) .unwrap() - .render_to_string(w, parent_scope) + .render_to_string(w, parent_scope, hydratable) .await; + + if hydratable { + w.push_str(""); + } } } } diff --git a/packages/yew/src/virtual_dom/vlist.rs b/packages/yew/src/virtual_dom/vlist.rs index 30ca06c2f92..eda8c4f2eea 100644 --- a/packages/yew/src/virtual_dom/vlist.rs +++ b/packages/yew/src/virtual_dom/vlist.rs @@ -289,12 +289,17 @@ mod feat_ssr { use super::*; impl VList { - pub(crate) async fn render_to_string(&self, w: &mut String, parent_scope: &AnyScope) { + pub(crate) async fn render_to_string( + &self, + w: &mut String, + parent_scope: &AnyScope, + hydratable: bool, + ) { // Concurrently render all children. for fragment in futures::future::join_all(self.children.iter().map(|m| async move { let mut w = String::new(); - m.render_to_string(&mut w, parent_scope).await; + m.render_to_string(&mut w, parent_scope, hydratable).await; w })) diff --git a/packages/yew/src/virtual_dom/vnode.rs b/packages/yew/src/virtual_dom/vnode.rs index 86d18d041cc..6ef33462f5c 100644 --- a/packages/yew/src/virtual_dom/vnode.rs +++ b/packages/yew/src/virtual_dom/vnode.rs @@ -308,13 +308,20 @@ mod feat_ssr { &'a self, w: &'a mut String, parent_scope: &'a AnyScope, + hydratable: bool, ) -> LocalBoxFuture<'a, ()> { async move { match self { - VNode::VTag(vtag) => vtag.render_to_string(w, parent_scope).await, - VNode::VText(vtext) => vtext.render_to_string(w).await, - VNode::VComp(vcomp) => vcomp.render_to_string(w, parent_scope).await, - VNode::VList(vlist) => vlist.render_to_string(w, parent_scope).await, + VNode::VTag(vtag) => vtag.render_to_string(w, parent_scope, hydratable).await, + VNode::VText(vtext) => { + vtext.render_to_string(w, parent_scope, hydratable).await + } + VNode::VComp(vcomp) => { + vcomp.render_to_string(w, parent_scope, hydratable).await + } + VNode::VList(vlist) => { + vlist.render_to_string(w, parent_scope, hydratable).await + } // We are pretty safe here as it's not possible to get a web_sys::Node without DOM // support in the first place. // @@ -326,7 +333,9 @@ mod feat_ssr { // Portals are not rendered. VNode::VPortal(_) => {} VNode::VSuspense(vsuspense) => { - vsuspense.render_to_string(w, parent_scope).await + vsuspense + .render_to_string(w, parent_scope, hydratable) + .await } } } diff --git a/packages/yew/src/virtual_dom/vsuspense.rs b/packages/yew/src/virtual_dom/vsuspense.rs index e2a73bb1446..8663c8b647a 100644 --- a/packages/yew/src/virtual_dom/vsuspense.rs +++ b/packages/yew/src/virtual_dom/vsuspense.rs @@ -155,9 +155,23 @@ mod feat_ssr { use super::*; impl VSuspense { - pub(crate) async fn render_to_string(&self, w: &mut String, parent_scope: &AnyScope) { + pub(crate) async fn render_to_string( + &self, + w: &mut String, + parent_scope: &AnyScope, + hydratable: bool, + ) { + if hydratable { + w.push_str(""); + } // always render children on the server side. - self.children.render_to_string(w, parent_scope).await; + self.children + .render_to_string(w, parent_scope, hydratable) + .await; + + if hydratable { + w.push_str(""); + } } } } diff --git a/packages/yew/src/virtual_dom/vtag.rs b/packages/yew/src/virtual_dom/vtag.rs index e524e43c37a..019b8aa8b15 100644 --- a/packages/yew/src/virtual_dom/vtag.rs +++ b/packages/yew/src/virtual_dom/vtag.rs @@ -649,7 +649,12 @@ mod feat_ssr { use std::fmt::Write; impl VTag { - pub(crate) async fn render_to_string(&self, w: &mut String, parent_scope: &AnyScope) { + pub(crate) async fn render_to_string( + &self, + w: &mut String, + parent_scope: &AnyScope, + hydratable: bool, + ) { write!(w, "<{}", self.tag()).unwrap(); let write_attr = |w: &mut String, name: &str, val: Option<&str>| { @@ -680,7 +685,9 @@ mod feat_ssr { VTagInner::Input(_) => {} VTagInner::Textarea { .. } => { if let Some(m) = self.value() { - VText::new(m.to_owned()).render_to_string(w).await; + VText::new(m.to_owned()) + .render_to_string(w, parent_scope, hydratable) + .await; } w.push_str(""); @@ -690,7 +697,7 @@ mod feat_ssr { ref children, .. } => { - children.render_to_string(w, parent_scope).await; + children.render_to_string(w, parent_scope, hydratable).await; write!(w, "", tag).unwrap(); } diff --git a/packages/yew/src/virtual_dom/vtext.rs b/packages/yew/src/virtual_dom/vtext.rs index 4b458fccc34..6bc4c105142 100644 --- a/packages/yew/src/virtual_dom/vtext.rs +++ b/packages/yew/src/virtual_dom/vtext.rs @@ -33,7 +33,12 @@ mod feat_ssr { use super::*; impl VText { - pub(crate) async fn render_to_string(&self, w: &mut String) { + pub(crate) async fn render_to_string( + &self, + w: &mut String, + _parent_scope: &AnyScope, + _hydratable: bool, + ) { html_escape::encode_text_to_string(&self.text, w); } } From f2038c75862f2e20c10f243b9a93de9d8ed5992d Mon Sep 17 00:00:00 2001 From: Kaede Hoshikawa Date: Thu, 27 Jan 2022 12:46:25 +0900 Subject: [PATCH 03/40] Prints assistive node for root as well. --- packages/yew/src/html/component/scope.rs | 8 ++++++++ packages/yew/src/virtual_dom/vcomp.rs | 8 -------- 2 files changed, 8 insertions(+), 8 deletions(-) diff --git a/packages/yew/src/html/component/scope.rs b/packages/yew/src/html/component/scope.rs index 2c2f75697db..d4fe6ea4ae5 100644 --- a/packages/yew/src/html/component/scope.rs +++ b/packages/yew/src/html/component/scope.rs @@ -385,8 +385,16 @@ mod feat_ssr { ); scheduler::start(); + if hydratable { + w.push_str(""); + } + let html = rx.await.unwrap(); + if hydratable { + w.push_str(""); + } + let self_any_scope = self.to_any(); html.render_to_string(w, &self_any_scope, hydratable).await; diff --git a/packages/yew/src/virtual_dom/vcomp.rs b/packages/yew/src/virtual_dom/vcomp.rs index 444e0d50f75..709dc3fcaa4 100644 --- a/packages/yew/src/virtual_dom/vcomp.rs +++ b/packages/yew/src/virtual_dom/vcomp.rs @@ -323,20 +323,12 @@ mod feat_ssr { parent_scope: &AnyScope, hydratable: bool, ) { - if hydratable { - w.push_str(""); - } - self.mountable .as_ref() .map(|m| m.copy()) .unwrap() .render_to_string(w, parent_scope, hydratable) .await; - - if hydratable { - w.push_str(""); - } } } } From 2bb5701ef35a9dd6da3422b1e8a199636b2f793e Mon Sep 17 00:00:00 2001 From: Kaede Hoshikawa Date: Thu, 10 Feb 2022 21:22:19 +0900 Subject: [PATCH 04/40] Hydrate methods in AppHandle and Scope. --- packages/yew/src/app_handle.rs | 17 +++++ packages/yew/src/html/component/scope.rs | 91 ++++++++++++++++++++++++ packages/yew/src/renderer.rs | 2 +- 3 files changed, 109 insertions(+), 1 deletion(-) diff --git a/packages/yew/src/app_handle.rs b/packages/yew/src/app_handle.rs index 7f66c78ff45..0f2c6d0d388 100644 --- a/packages/yew/src/app_handle.rs +++ b/packages/yew/src/app_handle.rs @@ -33,6 +33,23 @@ where app } + pub(crate) fn hydrate_with_props(element: Element, props: Rc) -> Self { + let app = Self { + scope: Scope::new(None), + }; + + app.scope.hydrate_in_place( + element.clone(), + element + .first_child() + .expect("expected component, found EOF"), + NodeRef::default(), + props, + ); + + app + } + /// Schedule the app for destruction pub fn destroy(mut self) { self.scope.destroy() diff --git a/packages/yew/src/html/component/scope.rs b/packages/yew/src/html/component/scope.rs index d4fe6ea4ae5..b444edf6406 100644 --- a/packages/yew/src/html/component/scope.rs +++ b/packages/yew/src/html/component/scope.rs @@ -472,6 +472,97 @@ mod feat_io { } } +#[cfg_attr(documenting, doc(cfg(feature = "hydration")))] +#[cfg(feature = "hydration")] +mod feat_hydration { + use super::*; + + impl Scope { + /// Hydrates the component. + /// + /// Returns the NodeRef of the next sibling. + /// + /// # Note + /// + /// This method is expected to collect all the elements belongs to the current component + /// immediately. + pub(crate) fn hydrate_in_place( + &self, + parent: Element, + first_node: Node, + node_ref: NodeRef, + props: Rc, + ) -> NodeRef { + assert_eq!( + first_node.node_type(), + Node::COMMENT_NODE, + // TODO: improve error message with human readable node type name. + "expected component start, found node type {}", + first_node.node_type() + ); + + let mut nodes = Vec::new(); + + if first_node.text_content().unwrap_or_else(|| "".to_string()) != "yew-comp-start" { + panic!("expected comment start, found comment node"); + } + + let mut current_node = first_node; + let mut nested_layers = 1; + + loop { + current_node = current_node + .next_sibling() + .expect("expected component end, found EOF"); + + if current_node.node_type() == Node::COMMENT_NODE { + let text_content = current_node + .text_content() + .unwrap_or_else(|| "".to_string()); + + if text_content == "yew-comp-start" { + // We found another component, we need to increase component counter. + nested_layers += 1; + } else if text_content == "yew-comp-end" { + // We found a component end, minus component counter. + nested_layers -= 1; + if nested_layers == 0 { + // We have found the component end of the current component, breaking + // the loop. + break; + } + } + } + + nodes.push(current_node.clone()); + } + + let next_sibling = NodeRef::default(); + next_sibling.set(current_node.next_sibling()); + + scheduler::push_component_create( + CreateRunner { + parent: Some(parent), + next_sibling: next_sibling.clone(), + placeholder: VNode::default(), + node_ref, + props, + scope: self.clone(), + #[cfg(feature = "ssr")] + html_sender: None, + }, + RenderRunner { + state: self.state.clone(), + }, + ); + // Not guaranteed to already have the scheduler started + scheduler::start(); + + next_sibling + } + } +} + /// Defines a message type that can be sent to a component. /// Used for the return value of closure given to [Scope::batch_callback](struct.Scope.html#method.batch_callback). pub trait SendAsMessage { diff --git a/packages/yew/src/renderer.rs b/packages/yew/src/renderer.rs index 09bd76d2089..4f5db36a438 100644 --- a/packages/yew/src/renderer.rs +++ b/packages/yew/src/renderer.rs @@ -102,7 +102,7 @@ mod feat_hydration { /// Hydrates the application. pub fn hydrate(self) -> AppHandle { set_default_panic_hook(); - todo!() + AppHandle::::hydrate_with_props(self.root, Rc::new(self.props)) } } } From 336f66bd8a72c21887f4b01436b6d213e33717b0 Mon Sep 17 00:00:00 2001 From: Kaede Hoshikawa Date: Thu, 10 Feb 2022 22:38:00 +0900 Subject: [PATCH 05/40] Hydrate & ComponentState Refactoring (clippy). --- packages/yew/Cargo.toml | 1 + packages/yew/src/app_handle.rs | 43 ++++--- packages/yew/src/html/component/lifecycle.rs | 119 ++++++++++++------- packages/yew/src/html/component/scope.rs | 30 +++-- packages/yew/src/virtual_dom/mod.rs | 39 ++++++ packages/yew/src/virtual_dom/vcomp.rs | 23 ++++ packages/yew/src/virtual_dom/vlist.rs | 23 ++++ packages/yew/src/virtual_dom/vnode.rs | 35 ++++++ packages/yew/src/virtual_dom/vsuspense.rs | 23 ++++ packages/yew/src/virtual_dom/vtag.rs | 23 ++++ packages/yew/src/virtual_dom/vtext.rs | 23 ++++ 11 files changed, 312 insertions(+), 70 deletions(-) diff --git a/packages/yew/Cargo.toml b/packages/yew/Cargo.toml index 06ca4942910..fe58fd6d213 100644 --- a/packages/yew/Cargo.toml +++ b/packages/yew/Cargo.toml @@ -51,6 +51,7 @@ features = [ "Location", "MouseEvent", "Node", + "NodeList", "PointerEvent", "ProgressEvent", "Text", diff --git a/packages/yew/src/app_handle.rs b/packages/yew/src/app_handle.rs index d2940541652..a9a6bdd84ec 100644 --- a/packages/yew/src/app_handle.rs +++ b/packages/yew/src/app_handle.rs @@ -33,23 +33,6 @@ where app } - pub(crate) fn hydrate_with_props(element: Element, props: Rc) -> Self { - let app = Self { - scope: Scope::new(None), - }; - - app.scope.hydrate_in_place( - element.clone(), - element - .first_child() - .expect("expected component, found EOF"), - NodeRef::default(), - props, - ); - - app - } - /// Schedule the app for destruction pub fn destroy(mut self) { self.scope.destroy(false) @@ -67,6 +50,32 @@ where } } +#[cfg_attr(documenting, doc(cfg(feature = "hydration")))] +#[cfg(feature = "hydration")] +mod feat_hydration { + use super::*; + + use crate::virtual_dom::collect_child_nodes; + + impl AppHandle + where + COMP: BaseComponent, + { + pub(crate) fn hydrate_with_props(element: Element, props: Rc) -> Self { + let app = Self { + scope: Scope::new(None), + }; + + let mut fragment = collect_child_nodes(&element); + + app.scope + .hydrate_in_place(element, &mut fragment, NodeRef::default(), props); + + app + } + } +} + /// Removes anything from the given element. fn clear_element(element: &Element) { while let Some(child) = element.last_child() { diff --git a/packages/yew/src/html/component/lifecycle.rs b/packages/yew/src/html/component/lifecycle.rs index 15595b91de0..1d2ec607594 100644 --- a/packages/yew/src/html/component/lifecycle.rs +++ b/packages/yew/src/html/component/lifecycle.rs @@ -4,14 +4,20 @@ use super::{AnyScope, BaseComponent, Scope}; use crate::html::{RenderError, RenderResult}; use crate::scheduler::{self, Runnable, Shared}; use crate::suspense::{Suspense, Suspension}; +#[cfg(feature = "hydration")] +use crate::virtual_dom::VHydrate; use crate::virtual_dom::{VDiff, VNode}; use crate::Callback; use crate::{Context, NodeRef}; #[cfg(feature = "ssr")] use futures::channel::oneshot; use std::any::Any; +#[cfg(feature = "hydration")] +use std::collections::VecDeque; use std::rc::Rc; use web_sys::Element; +#[cfg(feature = "hydration")] +use web_sys::Node; pub(crate) struct CompStateInner where @@ -107,6 +113,9 @@ pub(crate) struct ComponentState { suspension: Option, + #[cfg(feature = "hydration")] + hydrate_fragment: Option>, + #[cfg(feature = "ssr")] html_sender: Option>, @@ -115,43 +124,6 @@ pub(crate) struct ComponentState { pub(crate) vcomp_id: usize, } -impl ComponentState { - pub(crate) fn new( - parent: Option, - next_sibling: NodeRef, - root_node: VNode, - node_ref: NodeRef, - scope: Scope, - props: Rc, - #[cfg(feature = "ssr")] html_sender: Option>, - ) -> Self { - #[cfg(debug_assertions)] - let vcomp_id = scope.vcomp_id; - let context = Context { scope, props }; - - let inner = Box::new(CompStateInner { - component: COMP::create(&context), - context, - }); - - Self { - inner, - root_node, - parent, - next_sibling, - node_ref, - suspension: None, - has_rendered: false, - - #[cfg(feature = "ssr")] - html_sender, - - #[cfg(debug_assertions)] - vcomp_id, - } - } -} - pub(crate) struct CreateRunner { pub(crate) parent: Option, pub(crate) next_sibling: NodeRef, @@ -161,25 +133,63 @@ pub(crate) struct CreateRunner { pub(crate) scope: Scope, #[cfg(feature = "ssr")] pub(crate) html_sender: Option>, + #[cfg(feature = "hydration")] + pub(crate) hydrate_fragment: Option>, } impl Runnable for CreateRunner { fn run(self: Box) { + let scope = self.scope.clone(); + let mut current_state = self.scope.state.borrow_mut(); if current_state.is_none() { #[cfg(debug_assertions)] crate::virtual_dom::vcomp::log_event(self.scope.vcomp_id, "create"); - *current_state = Some(ComponentState::new( - self.parent, - self.next_sibling, - self.placeholder, - self.node_ref, - self.scope.clone(), - self.props, + let Self { + props, + placeholder, + parent, + next_sibling, + node_ref, + + #[cfg(feature = "hydration")] + hydrate_fragment, + + #[cfg(feature = "ssr")] + html_sender, + .. + } = *self; + + #[cfg(debug_assertions)] + let vcomp_id = scope.vcomp_id; + let context = Context { scope, props }; + + let inner = Box::new(CompStateInner { + component: COMP::create(&context), + context, + }); + + let state = ComponentState { + inner, + root_node: placeholder, + parent, + next_sibling, + node_ref, + suspension: None, + has_rendered: false, + + #[cfg(feature = "hydration")] + hydrate_fragment, + #[cfg(feature = "ssr")] - self.html_sender, - )); + html_sender, + + #[cfg(debug_assertions)] + vcomp_id, + }; + + *current_state = Some(state); } } } @@ -300,7 +310,24 @@ impl Runnable for RenderRunner { let scope = state.inner.any_scope(); let next_sibling = state.next_sibling.clone(); + #[cfg(not(feature = "hydration"))] let node = new_root.apply(&scope, m, next_sibling, ancestor); + + #[cfg(feature = "hydration")] + let node = match state.hydrate_fragment.take() { + Some(mut fragment) => { + let first_node = new_root.hydrate(&scope, m, &mut fragment); + + assert!( + fragment.front().is_none(), + "expected end of component, found node" + ); + + first_node + } + None => new_root.apply(&scope, m, next_sibling, ancestor), + }; + state.node_ref.link(node); let first_render = !state.has_rendered; diff --git a/packages/yew/src/html/component/scope.rs b/packages/yew/src/html/component/scope.rs index 7d810c4d7b2..f66ecfcb324 100644 --- a/packages/yew/src/html/component/scope.rs +++ b/packages/yew/src/html/component/scope.rs @@ -293,6 +293,9 @@ impl Scope { scope: self.clone(), #[cfg(feature = "ssr")] html_sender: None, + + #[cfg(feature = "hydration")] + hydrate_fragment: None, }, RenderRunner { state: self.state.clone(), @@ -416,6 +419,9 @@ mod feat_ssr { props, scope: self.clone(), html_sender: Some(tx), + + #[cfg(feature = "hydration")] + hydrate_fragment: None, }, RenderRunner { state: self.state.clone(), @@ -515,6 +521,7 @@ mod feat_io { #[cfg(feature = "hydration")] mod feat_hydration { use super::*; + use std::collections::VecDeque; impl Scope { /// Hydrates the component. @@ -525,13 +532,20 @@ mod feat_hydration { /// /// This method is expected to collect all the elements belongs to the current component /// immediately. + /// + /// We don't remove the comment node at the moment as it's needed to maintain the + /// structure. pub(crate) fn hydrate_in_place( &self, parent: Element, - first_node: Node, + fragment: &mut VecDeque, node_ref: NodeRef, props: Rc, ) -> NodeRef { + let first_node = fragment + .pop_front() + .expect("expected component start, found EOF"); + assert_eq!( first_node.node_type(), Node::COMMENT_NODE, @@ -540,18 +554,18 @@ mod feat_hydration { first_node.node_type() ); - let mut nodes = Vec::new(); + let mut nodes = VecDeque::new(); if first_node.text_content().unwrap_or_else(|| "".to_string()) != "yew-comp-start" { panic!("expected comment start, found comment node"); } - let mut current_node = first_node; + let mut current_node; let mut nested_layers = 1; loop { - current_node = current_node - .next_sibling() + current_node = fragment + .pop_front() .expect("expected component end, found EOF"); if current_node.node_type() == Node::COMMENT_NODE { @@ -573,11 +587,12 @@ mod feat_hydration { } } - nodes.push(current_node.clone()); + nodes.push_back(current_node.clone()); } let next_sibling = NodeRef::default(); - next_sibling.set(current_node.next_sibling()); + // We register the first sibling, but we don't pop them from the fragment. + next_sibling.set(fragment.front().cloned()); scheduler::push_component_create( CreateRunner { @@ -589,6 +604,7 @@ mod feat_hydration { scope: self.clone(), #[cfg(feature = "ssr")] html_sender: None, + hydrate_fragment: Some(nodes), }, RenderRunner { state: self.state.clone(), diff --git a/packages/yew/src/virtual_dom/mod.rs b/packages/yew/src/virtual_dom/mod.rs index 94e637c57cd..d1eea3a6493 100644 --- a/packages/yew/src/virtual_dom/mod.rs +++ b/packages/yew/src/virtual_dom/mod.rs @@ -541,6 +541,45 @@ pub(crate) trait VDiff { ) -> NodeRef; } +#[cfg_attr(documenting, doc(cfg(feature = "hydration")))] +#[cfg(feature = "hydration")] +mod feat_hydration { + use super::*; + + use std::collections::VecDeque; + + /// This trait provides features to hydrate a fragment. + pub(crate) trait VHydrate { + /// hydrates current tree. + /// + /// Returns a reference to the first node of the hydrated tree. + fn hydrate( + &mut self, + parent_scope: &AnyScope, + parent: &Element, + fragment: &mut VecDeque, + ) -> NodeRef; + } + + pub(crate) fn collect_child_nodes(parent: &Node) -> VecDeque { + let mut fragment = VecDeque::with_capacity(parent.child_nodes().length() as usize); + + let mut current_node = parent.first_child(); + + // This is easier than iterating child nodes at the moment + // as we don't have to downcast iterator values and minimises dom access. + while let Some(m) = current_node { + current_node = m.next_sibling(); + fragment.push_back(m); + } + + fragment + } +} + +#[cfg(feature = "hydration")] +pub(crate) use feat_hydration::*; + pub(crate) fn insert_node(node: &Node, parent: &Element, next_sibling: Option<&Node>) { match next_sibling { Some(next_sibling) => parent diff --git a/packages/yew/src/virtual_dom/vcomp.rs b/packages/yew/src/virtual_dom/vcomp.rs index 7921ac76262..4d2e9fb587c 100644 --- a/packages/yew/src/virtual_dom/vcomp.rs +++ b/packages/yew/src/virtual_dom/vcomp.rs @@ -330,6 +330,29 @@ mod feat_ssr { } } +#[cfg_attr(documenting, doc(cfg(feature = "hydration")))] +#[cfg(feature = "hydration")] +mod feat_hydration { + use super::*; + + use std::collections::VecDeque; + + use web_sys::Node; + + use crate::virtual_dom::VHydrate; + + impl VHydrate for VComp { + fn hydrate( + &mut self, + parent_scope: &AnyScope, + parent: &Element, + fragment: &mut VecDeque, + ) -> NodeRef { + todo!() + } + } +} + #[cfg(test)] mod tests { use super::*; diff --git a/packages/yew/src/virtual_dom/vlist.rs b/packages/yew/src/virtual_dom/vlist.rs index f01473ab8cc..77ff7bfe1c7 100644 --- a/packages/yew/src/virtual_dom/vlist.rs +++ b/packages/yew/src/virtual_dom/vlist.rs @@ -380,6 +380,29 @@ impl VDiff for VList { } } +#[cfg_attr(documenting, doc(cfg(feature = "hydration")))] +#[cfg(feature = "hydration")] +mod feat_hydration { + use super::*; + + use std::collections::VecDeque; + + use web_sys::Node; + + use crate::virtual_dom::VHydrate; + + impl VHydrate for VList { + fn hydrate( + &mut self, + parent_scope: &AnyScope, + parent: &Element, + fragment: &mut VecDeque, + ) -> NodeRef { + todo!() + } + } +} + #[cfg(test)] mod layout_tests { extern crate self as yew; diff --git a/packages/yew/src/virtual_dom/vnode.rs b/packages/yew/src/virtual_dom/vnode.rs index bb2cc579af6..2c81f191e3c 100644 --- a/packages/yew/src/virtual_dom/vnode.rs +++ b/packages/yew/src/virtual_dom/vnode.rs @@ -345,6 +345,41 @@ mod feat_ssr { } } +#[cfg_attr(documenting, doc(cfg(feature = "hydration")))] +#[cfg(feature = "hydration")] +mod feat_hydration { + use super::*; + + use std::collections::VecDeque; + + use crate::virtual_dom::VHydrate; + + impl VHydrate for VNode { + fn hydrate( + &mut self, + parent_scope: &AnyScope, + parent: &Element, + fragment: &mut VecDeque, + ) -> NodeRef { + match self { + VNode::VTag(vtag) => vtag.hydrate(parent_scope, parent, fragment), + VNode::VText(vtext) => vtext.hydrate(parent_scope, parent, fragment), + VNode::VComp(vcomp) => vcomp.hydrate(parent_scope, parent, fragment), + VNode::VList(vlist) => vlist.hydrate(parent_scope, parent, fragment), + // You cannot hydrate a VRef. + VNode::VRef(_) => { + panic!("VRef is not hydratable. Try move it to a component mounted after an effect.") + } + // Portals are not rendered. + VNode::VPortal(_) => { + panic!("VPortal is not hydratable. Try move it to a component mounted after an effect.") + } + VNode::VSuspense(vsuspense) => vsuspense.hydrate(parent_scope, parent, fragment), + } + } + } +} + #[cfg(test)] mod layout_tests { use super::*; diff --git a/packages/yew/src/virtual_dom/vsuspense.rs b/packages/yew/src/virtual_dom/vsuspense.rs index 76004876be0..a44d22597a7 100644 --- a/packages/yew/src/virtual_dom/vsuspense.rs +++ b/packages/yew/src/virtual_dom/vsuspense.rs @@ -150,6 +150,29 @@ impl VDiff for VSuspense { } } +#[cfg_attr(documenting, doc(cfg(feature = "hydration")))] +#[cfg(feature = "hydration")] +mod feat_hydration { + use super::*; + + use std::collections::VecDeque; + + use web_sys::Node; + + use crate::virtual_dom::VHydrate; + + impl VHydrate for VSuspense { + fn hydrate( + &mut self, + parent_scope: &AnyScope, + parent: &Element, + fragment: &mut VecDeque, + ) -> NodeRef { + todo!() + } + } +} + #[cfg(feature = "ssr")] mod feat_ssr { use super::*; diff --git a/packages/yew/src/virtual_dom/vtag.rs b/packages/yew/src/virtual_dom/vtag.rs index cf96c337096..8e5bdd7c4c8 100644 --- a/packages/yew/src/virtual_dom/vtag.rs +++ b/packages/yew/src/virtual_dom/vtag.rs @@ -711,6 +711,29 @@ mod feat_ssr { } } +#[cfg_attr(documenting, doc(cfg(feature = "hydration")))] +#[cfg(feature = "hydration")] +mod feat_hydration { + use super::*; + + use std::collections::VecDeque; + + use web_sys::Node; + + use crate::virtual_dom::VHydrate; + + impl VHydrate for VTag { + fn hydrate( + &mut self, + parent_scope: &AnyScope, + parent: &Element, + fragment: &mut VecDeque, + ) -> NodeRef { + todo!() + } + } +} + #[cfg(test)] mod tests { use super::*; diff --git a/packages/yew/src/virtual_dom/vtext.rs b/packages/yew/src/virtual_dom/vtext.rs index 4006c4e11c4..b4462555864 100644 --- a/packages/yew/src/virtual_dom/vtext.rs +++ b/packages/yew/src/virtual_dom/vtext.rs @@ -124,6 +124,29 @@ impl PartialEq for VText { } } +#[cfg_attr(documenting, doc(cfg(feature = "hydration")))] +#[cfg(feature = "hydration")] +mod feat_hydration { + use super::*; + + use std::collections::VecDeque; + + use web_sys::Node; + + use crate::virtual_dom::VHydrate; + + impl VHydrate for VText { + fn hydrate( + &mut self, + parent_scope: &AnyScope, + parent: &Element, + fragment: &mut VecDeque, + ) -> NodeRef { + todo!() + } + } +} + #[cfg(test)] mod test { extern crate self as yew; From 61b69e8e19a299e416dfdb127681cda30a6a61ca Mon Sep 17 00:00:00 2001 From: Kaede Hoshikawa Date: Thu, 10 Feb 2022 22:54:58 +0900 Subject: [PATCH 06/40] Hydrate VText. --- packages/yew/src/virtual_dom/vtext.rs | 32 +++++++++++++++++++++++++-- 1 file changed, 30 insertions(+), 2 deletions(-) diff --git a/packages/yew/src/virtual_dom/vtext.rs b/packages/yew/src/virtual_dom/vtext.rs index b4462555864..f56de1ecf97 100644 --- a/packages/yew/src/virtual_dom/vtext.rs +++ b/packages/yew/src/virtual_dom/vtext.rs @@ -134,15 +134,43 @@ mod feat_hydration { use web_sys::Node; use crate::virtual_dom::VHydrate; + use wasm_bindgen::JsCast; + + use crate::virtual_dom::insert_node; impl VHydrate for VText { fn hydrate( &mut self, - parent_scope: &AnyScope, + _parent_scope: &AnyScope, parent: &Element, fragment: &mut VecDeque, ) -> NodeRef { - todo!() + assert!( + self.reference.is_none(), + "trying to hydrate a mounted VText." + ); + + if let Some(m) = fragment.front().cloned() { + if m.node_type() == Node::TEXT_NODE { + if let Ok(m) = m.dyn_into::() { + // pop current node. + fragment.pop_front(); + + // always update node value, see reason below. + m.set_node_value(Some(self.text.as_ref())); + self.reference = Some(m.clone()); + + return NodeRef::new(m.into()); + } + } + } + + // If there are multiple text nodes placed back-to-back, it may be parsed as a single + // text node, hence we need to add extra text nodes here if the next node is not a text node. + let text_node = document().create_text_node(&self.text); + insert_node(&text_node, parent, fragment.front()); + self.reference = Some(text_node.clone()); + NodeRef::new(text_node.into()) } } } From 0b91f03382ec8e2fe44bf6b25398924c731fbd20 Mon Sep 17 00:00:00 2001 From: Kaede Hoshikawa Date: Thu, 10 Feb 2022 23:06:09 +0900 Subject: [PATCH 07/40] Fix ssr tests. --- .github/workflows/main-checks.yml | 31 ++++++++++++++++++++++- packages/yew/src/virtual_dom/vcomp.rs | 3 ++- packages/yew/src/virtual_dom/vlist.rs | 6 +++-- packages/yew/src/virtual_dom/vsuspense.rs | 3 ++- packages/yew/src/virtual_dom/vtag.rs | 15 +++++++---- packages/yew/src/virtual_dom/vtext.rs | 15 +++++++---- 6 files changed, 58 insertions(+), 15 deletions(-) diff --git a/.github/workflows/main-checks.yml b/.github/workflows/main-checks.yml index db27c709143..67f67565f98 100644 --- a/.github/workflows/main-checks.yml +++ b/.github/workflows/main-checks.yml @@ -159,6 +159,36 @@ jobs: args: --all-targets --workspace --exclude yew --exclude website-test + ssr_tests: + name: SSR Tests on ${{ matrix.toolchain }} + runs-on: ubuntu-latest + strategy: + matrix: + toolchain: + # anyway to dynamically grep the MSRV from Cargo.toml? + - 1.56.0 # MSRV + - stable + - nightly + + steps: + - uses: actions/checkout@v2 + with: + fetch-depth: 0 + + - uses: actions-rs/toolchain@v1 + with: + toolchain: ${{ matrix.toolchain }} + override: true + profile: minimal + + - uses: Swatinem/rust-cache@v1 + + - name: Run tests + uses: actions-rs/cargo@v1 + with: + command: test + args: --workspace ssr_tests --features ssr + test-lints: name: Test lints on nightly @@ -180,4 +210,3 @@ jobs: with: command: test args: -p yew-macro test_html_lints --features lints - diff --git a/packages/yew/src/virtual_dom/vcomp.rs b/packages/yew/src/virtual_dom/vcomp.rs index 4d2e9fb587c..09fc190765e 100644 --- a/packages/yew/src/virtual_dom/vcomp.rs +++ b/packages/yew/src/virtual_dom/vcomp.rs @@ -972,7 +972,8 @@ mod ssr_tests { } } - let renderer = ServerRenderer::::new(); + let mut renderer = ServerRenderer::::new(); + renderer.set_hydratable(false); let s = renderer.render().await; diff --git a/packages/yew/src/virtual_dom/vlist.rs b/packages/yew/src/virtual_dom/vlist.rs index 77ff7bfe1c7..0a537cd0504 100644 --- a/packages/yew/src/virtual_dom/vlist.rs +++ b/packages/yew/src/virtual_dom/vlist.rs @@ -1334,7 +1334,8 @@ mod ssr_tests { html! {
{"Hello "}{s}{"!"}
} } - let renderer = ServerRenderer::::new(); + let mut renderer = ServerRenderer::::new(); + renderer.set_hydratable(false); let s = renderer.render().await; @@ -1364,7 +1365,8 @@ mod ssr_tests { } } - let renderer = ServerRenderer::::new(); + let mut renderer = ServerRenderer::::new(); + renderer.set_hydratable(false); let s = renderer.render().await; diff --git a/packages/yew/src/virtual_dom/vsuspense.rs b/packages/yew/src/virtual_dom/vsuspense.rs index a44d22597a7..d857a201821 100644 --- a/packages/yew/src/virtual_dom/vsuspense.rs +++ b/packages/yew/src/virtual_dom/vsuspense.rs @@ -282,7 +282,8 @@ mod ssr_tests { let s = local .run_until(async move { - let renderer = ServerRenderer::::new(); + let mut renderer = ServerRenderer::::new(); + renderer.set_hydratable(false); renderer.render().await }) diff --git a/packages/yew/src/virtual_dom/vtag.rs b/packages/yew/src/virtual_dom/vtag.rs index 8e5bdd7c4c8..a6689ff86fb 100644 --- a/packages/yew/src/virtual_dom/vtag.rs +++ b/packages/yew/src/virtual_dom/vtag.rs @@ -1551,7 +1551,8 @@ mod ssr_tests { html! {
} } - let renderer = ServerRenderer::::new(); + let mut renderer = ServerRenderer::::new(); + renderer.set_hydratable(false); let s = renderer.render().await; @@ -1565,7 +1566,8 @@ mod ssr_tests { html! {
} } - let renderer = ServerRenderer::::new(); + let mut renderer = ServerRenderer::::new(); + renderer.set_hydratable(false); let s = renderer.render().await; @@ -1579,7 +1581,8 @@ mod ssr_tests { html! {
{"Hello!"}
} } - let renderer = ServerRenderer::::new(); + let mut renderer = ServerRenderer::::new(); + renderer.set_hydratable(false); let s = renderer.render().await; @@ -1593,7 +1596,8 @@ mod ssr_tests { html! {
{"Hello!"}
} } - let renderer = ServerRenderer::::new(); + let mut renderer = ServerRenderer::::new(); + renderer.set_hydratable(false); let s = renderer.render().await; @@ -1607,7 +1611,8 @@ mod ssr_tests { html! { +
+ +
+ + }) + } + + #[function_component(App)] + fn app() -> Html { + let fallback = html! {
{"wait..."}
}; + + html! { +
+ + + +
+ } + } + + let s = ServerRenderer::::new().render().await; + + gloo::utils::document() + .query_selector("#output") + .unwrap() + .unwrap() + .set_inner_html(&s); + + sleep(Duration::ZERO).await; + + Renderer::::with_root(gloo_utils::document().get_element_by_id("output").unwrap()) + .hydrate(); + + sleep(Duration::from_millis(10)).await; + + let result = obtain_result(); + + assert_eq!( + result.as_str(), + r#"
"# + ); + gloo_utils::document() + .query_selector(".take-a-break") + .unwrap() + .unwrap() + .dyn_into::() + .unwrap() + .click(); + + sleep(Duration::from_millis(10)).await; + + let result = obtain_result(); + assert_eq!(result.as_str(), "
wait...
"); + + sleep(Duration::from_millis(50)).await; + + let result = obtain_result(); + assert_eq!( + result.as_str(), + r#"
"# + ); +} + +#[wasm_bindgen_test] +async fn hydration_nested_suspense_works() { + #[derive(PartialEq)] + pub struct SleepState { + s: Suspension, + } + + impl SleepState { + fn new() -> Self { + let (s, handle) = Suspension::new(); + + spawn_local(async move { + sleep(Duration::from_millis(50)).await; + + handle.resume(); + }); + + Self { s } + } + } + + impl Reducible for SleepState { + type Action = (); + + fn reduce(self: Rc, _action: Self::Action) -> Rc { + Self::new().into() + } + } + + #[hook] + pub fn use_sleep() -> SuspensionResult> { + let sleep_state = use_reducer(SleepState::new); + + if sleep_state.s.resumed() { + Ok(Rc::new(move || sleep_state.dispatch(()))) + } else { + Err(sleep_state.s.clone()) + } + } + + #[function_component(InnerContent)] + fn inner_content() -> HtmlResult { + let resleep = use_sleep()?; + + let on_take_a_break = Callback::from(move |_: MouseEvent| (resleep.clone())()); + + Ok(html! { +
+
+ +
+
+ }) + } + + #[function_component(Content)] + fn content() -> HtmlResult { + let resleep = use_sleep()?; + + let fallback = html! {
{"wait...(inner)"}
}; + + let on_take_a_break = Callback::from(move |_: MouseEvent| (resleep.clone())()); + + Ok(html! { +
+
+ +
+ + + +
+ }) + } + + #[function_component(App)] + fn app() -> Html { + let fallback = html! {
{"wait...(outer)"}
}; + + html! { +
+ + + +
+ } + } + + let s = ServerRenderer::::new().render().await; + + gloo::utils::document() + .query_selector("#output") + .unwrap() + .unwrap() + .set_inner_html(&s); + + sleep(Duration::ZERO).await; + + Renderer::::with_root(gloo_utils::document().get_element_by_id("output").unwrap()) + .hydrate(); + + // outer suspense is hydrating... + sleep(Duration::from_millis(10)).await; + let result = obtain_result(); + assert_eq!( + result.as_str(), + r#"
"# + ); + + sleep(Duration::from_millis(50)).await; + + // inner suspense is hydrating... + let result = obtain_result(); + assert_eq!( + result.as_str(), + r#"
"# + ); + + sleep(Duration::from_millis(50)).await; + + // hydrated. + let result = obtain_result(); + assert_eq!( + result.as_str(), + r#"
"# + ); + + gloo_utils::document() + .query_selector(".take-a-break") + .unwrap() + .unwrap() + .dyn_into::() + .unwrap() + .click(); + + sleep(Duration::from_millis(10)).await; + + let result = obtain_result(); + assert_eq!(result.as_str(), "
wait...(outer)
"); + + sleep(Duration::from_millis(50)).await; + + let result = obtain_result(); + assert_eq!( + result.as_str(), + r#"
"# + ); + + gloo_utils::document() + .query_selector(".take-a-break2") + .unwrap() + .unwrap() + .dyn_into::() + .unwrap() + .click(); + + sleep(Duration::from_millis(10)).await; + let result = obtain_result(); + assert_eq!( + result.as_str(), + r#"
wait...(inner)
"# + ); + + sleep(Duration::from_millis(50)).await; + + let result = obtain_result(); + assert_eq!( + result.as_str(), + r#"
"# + ); +} From caa25ef557b6b364f35733dd00ba5e4bf8e97944 Mon Sep 17 00:00:00 2001 From: Kaede Hoshikawa Date: Sun, 13 Feb 2022 17:50:45 +0900 Subject: [PATCH 34/40] Cleanup the codebase. --- packages/yew/src/html/component/lifecycle.rs | 456 +++++++++++-------- packages/yew/src/html/component/scope.rs | 65 ++- packages/yew/src/suspense/component.rs | 27 +- packages/yew/src/virtual_dom/vnode.rs | 7 +- packages/yew/src/virtual_dom/vsuspense.rs | 309 +++++++------ packages/yew/tests/hydration.rs | 2 + 6 files changed, 470 insertions(+), 396 deletions(-) diff --git a/packages/yew/src/html/component/lifecycle.rs b/packages/yew/src/html/component/lifecycle.rs index 285720a87de..cc5bca83e3a 100644 --- a/packages/yew/src/html/component/lifecycle.rs +++ b/packages/yew/src/html/component/lifecycle.rs @@ -15,6 +15,45 @@ use std::any::Any; use std::rc::Rc; use web_sys::Element; +/// A State to track current component rendering status. +pub(crate) enum Rendered { + Render { + parent: Element, + + next_sibling: NodeRef, + node_ref: NodeRef, + + root_node: VNode, + }, + #[cfg(feature = "hydration")] + Hydration { + parent: Element, + + next_sibling: NodeRef, + node_ref: NodeRef, + + fragment: Fragment, + }, + #[cfg(feature = "ssr")] + Ssr { + sender: Option>, + }, +} + +impl Rendered { + pub fn root_vnode(&self) -> Option<&VNode> { + match self { + Rendered::Render { ref root_node, .. } => Some(root_node), + + #[cfg(feature = "hydration")] + Rendered::Hydration { .. } => None, + + #[cfg(feature = "ssr")] + Rendered::Ssr { .. } => None, + } + } +} + pub(crate) struct CompStateInner where COMP: BaseComponent, @@ -104,35 +143,17 @@ where pub(crate) struct ComponentState { pub(crate) inner: Box, - pub(crate) root_node: VNode, + pub(crate) rendered: Rendered, - /// When a component has no parent, it means that it should not be rendered. - parent: Option, - - next_sibling: NodeRef, - node_ref: NodeRef, has_rendered: bool, suspension: Option, - - #[cfg(feature = "hydration")] - hydrate_fragment: Option, - - #[cfg(feature = "ssr")] - html_sender: Option>, } pub(crate) struct CreateRunner { - pub(crate) parent: Option, - pub(crate) next_sibling: NodeRef, - pub(crate) placeholder: VNode, - pub(crate) node_ref: NodeRef, + pub(crate) rendered: Rendered, pub(crate) props: Rc, pub(crate) scope: Scope, - #[cfg(feature = "ssr")] - pub(crate) html_sender: Option>, - #[cfg(feature = "hydration")] - pub(crate) hydrate_fragment: Option, } impl Runnable for CreateRunner { @@ -145,18 +166,7 @@ impl Runnable for CreateRunner { crate::virtual_dom::vcomp::log_event(self.scope.id, "create"); let Self { - props, - placeholder, - parent, - next_sibling, - node_ref, - - #[cfg(feature = "hydration")] - hydrate_fragment, - - #[cfg(feature = "ssr")] - html_sender, - .. + props, rendered, .. } = *self; let context = Context { scope, props }; @@ -168,18 +178,9 @@ impl Runnable for CreateRunner { let state = ComponentState { inner, - root_node: placeholder, - parent, - next_sibling, - node_ref, + rendered, suspension: None, has_rendered: false, - - #[cfg(feature = "hydration")] - hydrate_fragment, - - #[cfg(feature = "ssr")] - html_sender, }; *current_state = Some(state); @@ -203,46 +204,78 @@ pub(crate) struct UpdateRunner { impl Runnable for UpdateRunner { fn run(self: Box) { - if let Some(mut state) = self.state.borrow_mut().as_mut() { + if let Some(state) = self.state.borrow_mut().as_mut() { let schedule_render = match self.event { UpdateEvent::Message => state.inner.flush_messages(), - UpdateEvent::Properties(props, node_ref, next_sibling) => { - // When components are updated, a new node ref could have been passed in - state.node_ref = node_ref; - // When components are updated, their siblings were likely also updated - state.next_sibling = next_sibling; - // Only trigger changed if props were changed - - state.inner.props_changed(props) - } - UpdateEvent::Shift(parent, next_sibling) => { - // We need to shift the hydrate fragment if the component is not hydrated. - #[cfg(feature = "hydration")] - { - if let Some(ref m) = state.hydrate_fragment { - m.shift( - state.parent.as_ref().unwrap(), - &parent, - next_sibling.clone(), - ); - } else { - state.root_node.shift( - state.parent.as_ref().unwrap(), - &parent, - next_sibling.clone(), - ); + UpdateEvent::Properties(props, next_node_ref, next_sibling) => { + match state.rendered { + Rendered::Render { + ref mut node_ref, + next_sibling: ref mut current_next_sibling, + .. + } => { + // When components are updated, a new node ref could have been passed in + *node_ref = next_node_ref; + // 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 = "hydration")] + Rendered::Hydration { + ref mut node_ref, + next_sibling: ref mut current_next_sibling, + .. + } => { + // When components are updated, a new node ref could have been passed in + *node_ref = next_node_ref; + // 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")] + Rendered::Ssr { .. } => state.inner.props_changed(props), } + } + + UpdateEvent::Shift(next_parent, next_sibling) => { + match state.rendered { + Rendered::Render { + ref root_node, + ref mut parent, + next_sibling: ref mut current_next_sibling, + .. + } => { + root_node.shift(parent, &next_parent, next_sibling.clone()); + + *parent = next_parent; + *current_next_sibling = next_sibling; + } - #[cfg(not(feature = "hydration"))] - state.root_node.shift( - state.parent.as_ref().unwrap(), - &parent, - next_sibling.clone(), - ); + // We need to shift the hydrate fragment if the component is not hydrated. + #[cfg(feature = "hydration")] + Rendered::Hydration { + ref fragment, + ref mut parent, + next_sibling: ref mut current_next_sibling, + .. + } => { + fragment.shift(parent, &next_parent, next_sibling.clone()); + + *parent = next_parent; + *current_next_sibling = next_sibling; + } - state.parent = Some(parent); - state.next_sibling = next_sibling; + // Shifting is not possible during SSR. + #[cfg(feature = "ssr")] + Rendered::Ssr { .. } => { + #[cfg(debug_assertions)] + panic!("shifting is not possible during SSR"); + } + } false } @@ -280,9 +313,36 @@ impl Runnable for DestroyRunner { state.inner.destroy(); - if let Some(ref m) = state.parent { - state.root_node.detach(m, self.parent_to_detach); - state.node_ref.set(None); + match state.rendered { + Rendered::Render { + ref mut root_node, + ref parent, + ref node_ref, + .. + } => { + root_node.detach(parent, self.parent_to_detach); + + node_ref.set(None); + } + // We need to detach the hydrate fragment if the component is not hydrated. + #[cfg(feature = "hydration")] + Rendered::Hydration { + ref fragment, + ref parent, + ref node_ref, + .. + } => { + for node in fragment.iter() { + parent + .remove_child(node) + .expect("failed to remove fragment node."); + } + + node_ref.set(None); + } + + #[cfg(feature = "ssr")] + Rendered::Ssr { .. } => {} } } } @@ -293,14 +353,58 @@ pub(crate) struct RenderRunner { } impl RenderRunner { - #[cfg(feature = "hydration")] - fn render_html(&self, parent: &Element, state: &mut ComponentState, ancestor: VNode) { - let new_root = &mut state.root_node; + fn render(&self, state: &mut ComponentState, new_root: VNode) { + // Currently not suspended, we remove any previous suspension and update + // normally. + + if let Some(m) = state.suspension.take() { + let comp_scope = state.inner.any_scope(); + + let suspense_scope = comp_scope.find_parent_scope::().unwrap(); + let suspense = suspense_scope.get_component().unwrap(); + + suspense.resume(m); + } + let scope = state.inner.any_scope(); - let next_sibling = state.next_sibling.clone(); - match state.hydrate_fragment.take() { - Some(mut fragment) => { + match state.rendered { + Rendered::Render { + root_node: ref mut current_root, + ref parent, + ref next_sibling, + ref node_ref, + .. + } => { + let mut root = new_root; + std::mem::swap(&mut root, current_root); + + let ancestor = root; + + let node = current_root.apply(&scope, parent, next_sibling.clone(), Some(ancestor)); + + node_ref.link(node); + + let first_render = !state.has_rendered; + state.has_rendered = true; + + scheduler::push_component_rendered( + state.inner.id(), + RenderedRunner { + state: self.state.clone(), + first_render, + }, + first_render, + ); + } + + #[cfg(feature = "hydration")] + Rendered::Hydration { + ref mut fragment, + ref parent, + ref node_ref, + ref next_sibling, + } => { // We schedule a "first" render to run immediately after hydration, // for the following reason: // 1. Fix NodeRef (first_node and next_sibling) @@ -313,59 +417,83 @@ impl RenderRunner { }, ); + let mut root = new_root; + // This first node is not guaranteed to be correct here. // As it may be a comment node that is removed afterwards. // but we link it anyways. - let node = new_root.hydrate(&scope, parent, &mut fragment); + let node = root.hydrate(&scope, parent, fragment); // We trim all text nodes before checking as it's likely these are whitespaces. fragment.trim_start_text_nodes(parent); assert!(fragment.is_empty(), "expected end of component, found node"); - state.node_ref.link(node); - } - None => { - let node = new_root.apply(&scope, parent, next_sibling, Some(ancestor)); - - state.node_ref.link(node); + node_ref.link(node); - let first_render = !state.has_rendered; - state.has_rendered = true; + state.rendered = Rendered::Render { + root_node: root, + parent: parent.clone(), + node_ref: node_ref.clone(), + next_sibling: next_sibling.clone(), + }; + } - scheduler::push_component_rendered( - state.inner.id(), - RenderedRunner { - state: self.state.clone(), - first_render, - }, - first_render, - ); + #[cfg(feature = "ssr")] + Rendered::Ssr { ref mut sender } => { + if let Some(tx) = sender.take() { + tx.send(new_root).unwrap(); + } } }; } - #[cfg(not(feature = "hydration"))] - fn render_html(&self, parent: &Element, state: &mut ComponentState, ancestor: VNode) { - let new_root = &mut state.root_node; - let scope = state.inner.any_scope(); - let next_sibling = state.next_sibling.clone(); + fn suspend(&self, state: &mut ComponentState, suspension: Suspension) { + // Currently suspended, we re-use previous root node and send + // suspension to parent element. + let shared_state = self.state.clone(); - let node = new_root.apply(&scope, parent, next_sibling, Some(ancestor)); + if suspension.resumed() { + // schedule a render immediately if suspension is resumed. - state.node_ref.link(node); + scheduler::push_component_render( + state.inner.id(), + RenderRunner { + state: shared_state, + }, + ); + } else { + // We schedule a render after current suspension is resumed. - let first_render = !state.has_rendered; - state.has_rendered = true; + let comp_scope = state.inner.any_scope(); - scheduler::push_component_rendered( - state.inner.id(), - RenderedRunner { - state: self.state.clone(), - first_render, - }, - first_render, - ); + let suspense_scope = comp_scope + .find_parent_scope::() + .expect("To suspend rendering, a component is required."); + let suspense = suspense_scope.get_component().unwrap(); + + let comp_id = state.inner.id(); + + suspension.listen(Callback::from(move |_| { + scheduler::push_component_render( + comp_id, + RenderRunner { + state: shared_state.clone(), + }, + ); + scheduler::start(); + })); + + if let Some(ref last_suspension) = state.suspension { + if &suspension != last_suspension { + // We remove previous suspension from the suspense. + suspense.resume(last_suspension.clone()); + } + } + state.suspension = Some(suspension.clone()); + + suspense.suspend(suspension); + } } } @@ -376,80 +504,8 @@ impl Runnable for RenderRunner { crate::virtual_dom::vcomp::log_event(state.inner.id(), "render"); match state.inner.view() { - Ok(m) => { - // Currently not suspended, we remove any previous suspension and update - // normally. - let mut root = m; - if state.parent.is_some() { - std::mem::swap(&mut root, &mut state.root_node); - } - - if let Some(m) = state.suspension.take() { - let comp_scope = state.inner.any_scope(); - - let suspense_scope = comp_scope.find_parent_scope::().unwrap(); - let suspense = suspense_scope.get_component().unwrap(); - - suspense.resume(m); - } - - if let Some(ref m) = state.parent.clone() { - self.render_html(m, state, root); - } else { - #[cfg(feature = "ssr")] - if let Some(tx) = state.html_sender.take() { - tx.send(root).unwrap(); - } - } - } - - Err(RenderError::Suspended(m)) => { - // Currently suspended, we re-use previous root node and send - // suspension to parent element. - let shared_state = self.state.clone(); - - if m.resumed() { - // schedule a render immediately if suspension is resumed. - - scheduler::push_component_render( - state.inner.id(), - RenderRunner { - state: shared_state, - }, - ); - } else { - // We schedule a render after current suspension is resumed. - - let comp_scope = state.inner.any_scope(); - - let suspense_scope = comp_scope - .find_parent_scope::() - .expect("To suspend rendering, a component is required."); - let suspense = suspense_scope.get_component().unwrap(); - - let comp_id = state.inner.id(); - - m.listen(Callback::from(move |_| { - scheduler::push_component_render( - comp_id, - RenderRunner { - state: shared_state.clone(), - }, - ); - scheduler::start(); - })); - - if let Some(ref last_m) = state.suspension { - if &m != last_m { - // We remove previous suspension from the suspense. - suspense.resume(last_m.clone()); - } - } - state.suspension = Some(m.clone()); - - suspense.suspend(m); - } - } + Ok(m) => self.render(state, m), + Err(RenderError::Suspended(m)) => self.suspend(state, m), }; } } @@ -466,8 +522,18 @@ impl Runnable for RenderedRunner { #[cfg(debug_assertions)] crate::virtual_dom::vcomp::log_event(state.inner.id(), "rendered"); - if state.suspension.is_none() && state.parent.is_some() { - state.inner.rendered(self.first_render); + match state.rendered { + #[cfg(feature = "ssr")] + Rendered::Ssr { .. } => {} + #[cfg(feature = "hydration")] + Rendered::Hydration { .. } => {} + + // We only call rendered when the component is rendered & not suspended.. + Rendered::Render { .. } => { + if state.suspension.is_none() { + state.inner.rendered(self.first_render); + } + } } } } diff --git a/packages/yew/src/html/component/scope.rs b/packages/yew/src/html/component/scope.rs index 6b2698e152e..a22adeb65a2 100644 --- a/packages/yew/src/html/component/scope.rs +++ b/packages/yew/src/html/component/scope.rs @@ -1,12 +1,10 @@ //! Component scope module -use super::{ - lifecycle::{ - CompStateInner, ComponentState, CreateRunner, DestroyRunner, RenderRunner, UpdateEvent, - UpdateRunner, - }, - BaseComponent, +use super::lifecycle::{ + CompStateInner, ComponentState, CreateRunner, DestroyRunner, RenderRunner, Rendered, + UpdateEvent, UpdateRunner, }; +use super::BaseComponent; use crate::callback::Callback; use crate::context::{ContextHandle, ContextProvider}; use crate::html::NodeRef; @@ -162,7 +160,12 @@ impl Scoped for Scope { state_ref.as_ref()?; Some(Ref::map(state_ref, |state_ref| { - &state_ref.as_ref().unwrap().root_node + state_ref + .as_ref() + .unwrap() + .rendered + .root_vnode() + .unwrap_or(VNode::EMPTY) })) } @@ -268,20 +271,19 @@ impl Scope { VNode::VRef(placeholder) }; + let rendered = Rendered::Render { + root_node: placeholder, + node_ref, + next_sibling, + parent, + }; + scheduler::push_component_create( self.id, CreateRunner { - parent: Some(parent), - next_sibling, - placeholder, - node_ref, + rendered, props, scope: self.clone(), - #[cfg(feature = "ssr")] - html_sender: None, - - #[cfg(feature = "hydration")] - hydrate_fragment: None, }, RenderRunner { state: self.state.clone(), @@ -397,19 +399,14 @@ mod feat_ssr { ) { let (tx, rx) = oneshot::channel(); + let rendered = Rendered::Ssr { sender: Some(tx) }; + scheduler::push_component_create( self.id, CreateRunner { - parent: None, - next_sibling: NodeRef::default(), - placeholder: VNode::default(), - node_ref: NodeRef::default(), + rendered, props, scope: self.clone(), - html_sender: Some(tx), - - #[cfg(feature = "hydration")] - hydrate_fragment: None, }, RenderRunner { state: self.state.clone(), @@ -556,22 +553,22 @@ mod feat_hydration { self.id ); - let nodes = Fragment::collect_between(fragment, &parent, "comp"); - node_ref.set(nodes.front().cloned()); - + let fragment = Fragment::collect_between(fragment, &parent, "comp"); + node_ref.set(fragment.front().cloned()); let next_sibling = NodeRef::default(); + let rendered = Rendered::Hydration { + parent, + node_ref, + next_sibling, + fragment, + }; + scheduler::push_component_hydrate( CreateRunner { - parent: Some(parent), - next_sibling, - placeholder: VNode::default(), - node_ref, + rendered, props, scope: self.clone(), - #[cfg(feature = "ssr")] - html_sender: None, - hydrate_fragment: Some(nodes), }, RenderRunner { state: self.state.clone(), diff --git a/packages/yew/src/suspense/component.rs b/packages/yew/src/suspense/component.rs index 66e35422a9a..7ed85f83ddc 100644 --- a/packages/yew/src/suspense/component.rs +++ b/packages/yew/src/suspense/component.rs @@ -1,5 +1,5 @@ use crate::html::{Children, Component, Context, Html, Properties, Scope}; -use crate::virtual_dom::{Key, VList, VNode, VSuspense}; +use crate::virtual_dom::{VList, VNode, VSuspense}; use web_sys::Element; @@ -12,9 +12,6 @@ pub struct SuspenseProps { #[prop_or_default] pub fallback: Html, - - #[prop_or_default] - pub key: Option, } #[derive(Debug)] @@ -73,23 +70,13 @@ impl Component for Suspense { } fn view(&self, ctx: &Context) -> Html { - let SuspenseProps { - children, - fallback: fallback_vnode, - key, - } = (*ctx.props()).clone(); - - let children_vnode = - VNode::from(VList::with_children(children.into_iter().collect(), None)); - - let vsuspense = VSuspense::new( - children_vnode, - fallback_vnode, - self.detached_parent.clone(), - !self.suspensions.is_empty(), - key, - ); + let SuspenseProps { children, fallback } = (*ctx.props()).clone(); + + let children = VNode::from(VList::with_children(children.into_iter().collect(), None)); + + let fallback = (!self.suspensions.is_empty()).then(|| fallback); + let vsuspense = VSuspense::new(children, fallback, self.detached_parent.clone()); VNode::from(vsuspense) } } diff --git a/packages/yew/src/virtual_dom/vnode.rs b/packages/yew/src/virtual_dom/vnode.rs index 9bd913c9d2c..cd9091a39b4 100644 --- a/packages/yew/src/virtual_dom/vnode.rs +++ b/packages/yew/src/virtual_dom/vnode.rs @@ -30,6 +30,8 @@ pub enum VNode { } impl VNode { + pub const EMPTY: &'static VNode = &VNode::VList(VList::new()); + pub fn key(&self) -> Option { match self { VNode::VComp(vcomp) => vcomp.key.clone(), @@ -38,7 +40,8 @@ impl VNode { VNode::VTag(vtag) => vtag.key.clone(), VNode::VText(_) => None, VNode::VPortal(vportal) => vportal.node.key(), - VNode::VSuspense(vsuspense) => vsuspense.key.clone(), + // VSuspenses are created by and is keyed by its VComp. + VNode::VSuspense(_) => None, } } @@ -50,7 +53,7 @@ impl VNode { VNode::VRef(_) | VNode::VText(_) => false, VNode::VTag(vtag) => vtag.key.is_some(), VNode::VPortal(vportal) => vportal.node.has_key(), - VNode::VSuspense(vsuspense) => vsuspense.key.is_some(), + VNode::VSuspense(_) => false, } } diff --git a/packages/yew/src/virtual_dom/vsuspense.rs b/packages/yew/src/virtual_dom/vsuspense.rs index 20e19d240cb..07d409c3af1 100644 --- a/packages/yew/src/virtual_dom/vsuspense.rs +++ b/packages/yew/src/virtual_dom/vsuspense.rs @@ -1,11 +1,18 @@ #[cfg(feature = "hydration")] use super::Fragment; -use super::{Key, VDiff, VNode}; +use super::{VDiff, VNode}; use crate::html::{AnyScope, NodeRef}; use web_sys::{Element, Node}; -#[cfg(not(feature = "hydration"))] -type Fragment = (); +/// An enum to Respresent Fallback UI for a VSuspense. +#[derive(Clone, Debug, PartialEq)] +enum VSuspenseFallback { + /// Suspense Fallback during Rendering + Render { root_node: Box }, + /// Suspense Fallback during Hydration + #[cfg(feature = "hydration")] + Hydration { fragment: Fragment }, +} /// This struct represents a suspendable DOM fragment. #[derive(Clone, Debug, PartialEq)] @@ -14,97 +21,84 @@ pub struct VSuspense { children: Box, /// Fallback nodes when suspended. - fallback: Box, + /// + /// None if not suspended. + fallback: Option, - /// The element to attach to when children is not attached to DOM detached_parent: Option, - - /// The fallback fragment when the suspense boundary is hydrating. - #[cfg(feature = "hydration")] - fallback_fragment: Option, - - /// Whether the current status is suspended. - suspended: bool, - - /// The Key. - pub(crate) key: Option, } impl VSuspense { pub(crate) fn new( children: VNode, - fallback: VNode, + fallback: Option, detached_parent: Option, - suspended: bool, - key: Option, ) -> Self { Self { children: children.into(), - fallback: fallback.into(), + fallback: fallback.map(|m| VSuspenseFallback::Render { + root_node: m.into(), + }), detached_parent, - suspended, - #[cfg(feature = "hydration")] - fallback_fragment: None, - key, } } pub(crate) fn first_node(&self) -> Option { - if self.suspended { - self.fallback.first_node() - } else { - self.children.first_node() + match self.fallback { + Some(VSuspenseFallback::Render { ref root_node, .. }) => root_node.first_node(), + + #[cfg(feature = "hydration")] + Some(VSuspenseFallback::Hydration { ref fragment, .. }) => fragment.front().cloned(), + + None => self.children.first_node(), } } } impl VDiff for VSuspense { fn detach(&mut self, parent: &Element, parent_to_detach: bool) { - if self.suspended { + let detached_parent = self.detached_parent.as_ref().expect("no detached parent?"); + + match self.fallback { + Some(VSuspenseFallback::Render { ref mut root_node }) => { + root_node.detach(parent, parent_to_detach); + self.children.detach(detached_parent, true); + } + #[cfg(feature = "hydration")] - { - if let Some(m) = self.fallback_fragment.take() { - if !parent_to_detach { - for node in m.iter() { - parent - .remove_child(node) - .expect("failed to remove child element"); - } + Some(VSuspenseFallback::Hydration { ref fragment }) => { + if !parent_to_detach { + for node in fragment.iter() { + parent + .remove_child(node) + .expect("failed to remove child element"); } - } else { - self.fallback.detach(parent, parent_to_detach); } - } - #[cfg(not(feature = "hydration"))] - self.fallback.detach(parent, parent_to_detach); + self.children.detach(detached_parent, true); + } - if let Some(ref m) = self.detached_parent { - self.children.detach(m, false); + None => { + self.children.detach(parent, parent_to_detach); } - } else { - self.children.detach(parent, parent_to_detach); } } fn shift(&self, previous_parent: &Element, next_parent: &Element, next_sibling: NodeRef) { - if self.suspended { + match self.fallback { + Some(VSuspenseFallback::Render { ref root_node }) => { + root_node.shift(previous_parent, next_parent, next_sibling); + } + #[cfg(feature = "hydration")] - { - if let Some(ref m) = self.fallback_fragment { - m.shift(previous_parent, next_parent, next_sibling); - } else { - self.fallback - .shift(previous_parent, next_parent, next_sibling); - } + Some(VSuspenseFallback::Hydration { ref fragment }) => { + fragment.shift(previous_parent, next_parent, next_sibling) } - #[cfg(not(feature = "hydration"))] - self.fallback - .shift(previous_parent, next_parent, next_sibling); - } else { - self.children - .shift(previous_parent, next_parent, next_sibling); + None => { + self.children + .shift(previous_parent, next_parent, next_sibling); + } } } @@ -117,117 +111,141 @@ impl VDiff for VSuspense { ) -> NodeRef { let detached_parent = self.detached_parent.as_ref().expect("no detached parent?"); - let (already_suspended, children_ancestor, fallback_ancestor, fallback_fragment) = - match ancestor { - Some(VNode::VSuspense(mut m)) => { - // We only preserve the child state if they are the same suspense. - if m.key != self.key || self.detached_parent != m.detached_parent { - m.detach(parent, false); - - (false, None, None, Option::::None) - } else { - ( - m.suspended, - Some(*m.children), - Some(*m.fallback), - #[cfg(feature = "hydration")] - m.fallback_fragment, - #[cfg(not(feature = "hydration"))] - None, - ) - } - } - Some(mut m) => { + let (children_ancestor, fallback_ancestor) = match ancestor { + Some(VNode::VSuspense(mut m)) => { + // We only preserve the child state if they are the same suspense. + if self.detached_parent != m.detached_parent { m.detach(parent, false); - (false, None, None, None) - } - None => (false, None, None, None), - }; - #[cfg(not(feature = "hydration"))] - let _ = fallback_fragment; + (None, None) + } else { + (Some(*m.children), m.fallback) + } + } + Some(mut m) => { + m.detach(parent, false); + (None, None) + } + None => (None, None), + }; // When it's suspended, we render children into an element that is detached from the dom // tree while rendering fallback UI into the original place where children resides in. - match (self.suspended, already_suspended) { - (true, true) => { - self.children.apply( - parent_scope, - detached_parent, - NodeRef::default(), - children_ancestor, - ); - - #[cfg(feature = "hydration")] - { - if fallback_fragment.is_none() { - self.fallback - .apply(parent_scope, parent, next_sibling, fallback_ancestor) - } else { + match (self.fallback.as_mut(), fallback_ancestor) { + // Currently Suspended, Continue to be Suspended. + (Some(fallback), Some(fallback_ancestor)) => { + match (fallback, fallback_ancestor) { + ( + VSuspenseFallback::Render { + root_node: ref mut fallback, + }, + VSuspenseFallback::Render { + root_node: fallback_ancestor, + }, + ) => { + self.children.apply( + parent_scope, + detached_parent, + NodeRef::default(), + children_ancestor, + ); + fallback.apply(parent_scope, parent, next_sibling, Some(*fallback_ancestor)) + } + + // current fallback cannot be Hydration. + #[cfg(feature = "hydration")] + (VSuspenseFallback::Hydration { .. }, VSuspenseFallback::Render { .. }) => { + panic!("invalid suspense state!") + } + + #[cfg(feature = "hydration")] + (_, VSuspenseFallback::Hydration { fragment }) => { + self.children.apply( + parent_scope, + detached_parent, + NodeRef::default(), + children_ancestor, + ); + let node_ref = NodeRef::default(); - node_ref.set(fallback_fragment.as_ref().and_then(|m| m.front().cloned())); + node_ref.set(fragment.front().cloned()); - self.fallback_fragment = fallback_fragment; + self.fallback = Some(VSuspenseFallback::Hydration { fragment }); node_ref } } - - #[cfg(not(feature = "hydration"))] - self.fallback - .apply(parent_scope, parent, next_sibling, fallback_ancestor) } - (false, false) => { + // Currently not Suspended, Continue to be not Suspended. + (None, None) => { self.children .apply(parent_scope, parent, next_sibling, children_ancestor) } - (true, false) => { - children_ancestor.as_ref().unwrap().shift( - parent, - detached_parent, - NodeRef::default(), - ); - - self.children.apply( - parent_scope, - detached_parent, - NodeRef::default(), - children_ancestor, - ); - - // first render of fallback, ancestor needs to be None. - self.fallback - .apply(parent_scope, parent, next_sibling, None) + // The children is about to be suspended. + (Some(fallback), None) => { + match fallback { + VSuspenseFallback::Render { + root_node: ref mut fallback, + } => { + if let Some(ref m) = children_ancestor { + m.shift(parent, detached_parent, NodeRef::default()); + } + + self.children.apply( + parent_scope, + detached_parent, + NodeRef::default(), + children_ancestor, + ); + + // first render of fallback, ancestor needs to be None. + fallback.apply(parent_scope, parent, next_sibling, None) + } + + // current fallback cannot be Hydration. + #[cfg(feature = "hydration")] + VSuspenseFallback::Hydration { .. } => { + panic!("invalid suspense state!") + } + } } - (false, true) => { - #[cfg(feature = "hydration")] - { - if let Some(m) = fallback_fragment { + // The children is about to be resumed. + (None, Some(fallback_ancestor)) => { + match fallback_ancestor { + VSuspenseFallback::Render { + root_node: mut fallback_ancestor, + } => { + fallback_ancestor.detach(parent, false); + + if let Some(ref m) = children_ancestor { + m.shift(detached_parent, parent, next_sibling.clone()); + } + + self.children + .apply(parent_scope, parent, next_sibling, children_ancestor) + } + + #[cfg(feature = "hydration")] + VSuspenseFallback::Hydration { fragment } => { // We can simply remove the fallback fragments it's not connected to // anything. - for node in m.iter() { + for node in fragment.iter() { parent .remove_child(node) .expect("failed to remove fragment node."); } - } else { - fallback_ancestor.unwrap().detach(parent, false); - } - } - #[cfg(not(feature = "hydration"))] - fallback_ancestor.unwrap().detach(parent, false); + if let Some(ref m) = children_ancestor { + m.shift(detached_parent, parent, next_sibling.clone()); + } - children_ancestor.as_ref().unwrap().shift( - detached_parent, - parent, - next_sibling.clone(), - ); - self.children - .apply(parent_scope, parent, next_sibling, children_ancestor) + self.children + .apply(parent_scope, parent, next_sibling, children_ancestor) + } + } } } } @@ -251,7 +269,6 @@ mod feat_hydration { // We start hydration with the VSuspense being suspended. // A subsequent render will resume the VSuspense if not needed to be suspended. - self.suspended = true; let fallback_nodes = Fragment::collect_between(fragment, parent, "suspense"); @@ -275,7 +292,9 @@ mod feat_hydration { .map(NodeRef::new) .unwrap_or_else(NodeRef::default); - self.fallback_fragment = Some(fallback_nodes); + self.fallback = Some(VSuspenseFallback::Hydration { + fragment: fallback_nodes, + }); first_node } diff --git a/packages/yew/tests/hydration.rs b/packages/yew/tests/hydration.rs index d3f4e2d4490..eb698114a81 100644 --- a/packages/yew/tests/hydration.rs +++ b/packages/yew/tests/hydration.rs @@ -1,3 +1,5 @@ +#![cfg(feature = "hydration")] + use std::rc::Rc; use std::time::Duration; From 2d8672b155cec52ab53487f7456cd0530c7f0133 Mon Sep 17 00:00:00 2001 From: Kaede Hoshikawa Date: Sun, 13 Feb 2022 18:42:01 +0900 Subject: [PATCH 35/40] Merge scheduler queue. --- examples/function_router/src/pages/post.rs | 6 ----- packages/yew/src/html/component/scope.rs | 3 ++- packages/yew/src/scheduler.rs | 28 ---------------------- 3 files changed, 2 insertions(+), 35 deletions(-) diff --git a/examples/function_router/src/pages/post.rs b/examples/function_router/src/pages/post.rs index e537ef1c110..e1ecbf4d101 100644 --- a/examples/function_router/src/pages/post.rs +++ b/examples/function_router/src/pages/post.rs @@ -46,12 +46,6 @@ pub fn Post(props: &Props) -> Html { ); } - log::error!( - "title: {}, seed: {}", - content::PostMeta::generate_from_seed(seed).title, - seed - ); - let post = &post.inner; let render_quote = |quote: &content::Quote| { diff --git a/packages/yew/src/html/component/scope.rs b/packages/yew/src/html/component/scope.rs index a22adeb65a2..35aa2b8c26b 100644 --- a/packages/yew/src/html/component/scope.rs +++ b/packages/yew/src/html/component/scope.rs @@ -564,7 +564,8 @@ mod feat_hydration { fragment, }; - scheduler::push_component_hydrate( + scheduler::push_component_create( + self.id, CreateRunner { rendered, props, diff --git a/packages/yew/src/scheduler.rs b/packages/yew/src/scheduler.rs index 5e6bfcb3204..de633b86be7 100644 --- a/packages/yew/src/scheduler.rs +++ b/packages/yew/src/scheduler.rs @@ -2,8 +2,6 @@ use std::cell::RefCell; use std::collections::BTreeMap; -#[cfg(feature = "hydration")] -use std::collections::VecDeque; use std::rc::Rc; /// Alias for Rc> @@ -27,9 +25,6 @@ struct Scheduler { create: Vec>, update: Vec>, - #[cfg(feature = "hydration")] - hydrate: VecDeque>, - /// A Binary Tree Map here guarantees components with lower id (parent) is rendered first and /// no more than 1 render can be scheduled before a component is rendered. /// @@ -77,18 +72,6 @@ pub(crate) fn push_component_create( }); } -/// Push a component creation and hydrate [Runnable]s to be executed -#[cfg(feature = "hydration")] -pub(crate) fn push_component_hydrate( - create: impl Runnable + 'static, - hydrate: impl Runnable + 'static, -) { - with(|s| { - s.create.push(Box::new(create)); - s.hydrate.push_back(Box::new(hydrate)); - }); -} - /// Push a component destruction [Runnable] to be executed pub(crate) fn push_component_destroy(runnable: impl Runnable + 'static) { with(|s| s.destroy.push(Box::new(runnable))); @@ -194,17 +177,6 @@ impl Scheduler { // Create events can be batched, as they are typically just for object creation to_run.append(&mut self.create); - #[cfg(feature = "hydration")] - { - // Hydration needs a higher priority than first render. - // They are both RenderRunnable, but hydration will schedule a "first" render after it's hydrated to - // fix NodeRef before rendered() can be called. First-render may not happen immediately if the component is - // suspended. - if let Some(r) = self.hydrate.pop_front() { - to_run.push(r); - } - } - // These typically do nothing and don't spawn any other events - can be batched. // Should be run only after all first renders have finished. if !to_run.is_empty() { From ec08c6be7e210e6f1531a209d8e407c3be10c7c7 Mon Sep 17 00:00:00 2001 From: Kaede Hoshikawa Date: Sun, 13 Feb 2022 20:00:32 +0900 Subject: [PATCH 36/40] Shorter opening and closing tags. --- packages/yew/src/html/component/lifecycle.rs | 10 ++-- packages/yew/src/html/component/marker.rs | 2 +- packages/yew/src/html/component/scope.rs | 17 +++--- packages/yew/src/virtual_dom/mod.rs | 56 +++++++++++--------- packages/yew/src/virtual_dom/vsuspense.rs | 7 +-- packages/yew/tests/hydration.rs | 6 +-- 6 files changed, 50 insertions(+), 48 deletions(-) diff --git a/packages/yew/src/html/component/lifecycle.rs b/packages/yew/src/html/component/lifecycle.rs index cc5bca83e3a..4659290bc1c 100644 --- a/packages/yew/src/html/component/lifecycle.rs +++ b/packages/yew/src/html/component/lifecycle.rs @@ -158,15 +158,17 @@ pub(crate) struct CreateRunner { impl Runnable for CreateRunner { fn run(self: Box) { - let scope = self.scope.clone(); + let state = self.scope.state.clone(); + let mut current_state = state.borrow_mut(); - let mut current_state = self.scope.state.borrow_mut(); if current_state.is_none() { #[cfg(debug_assertions)] crate::virtual_dom::vcomp::log_event(self.scope.id, "create"); let Self { - props, rendered, .. + props, + rendered, + scope, } = *self; let context = Context { scope, props }; @@ -528,7 +530,7 @@ impl Runnable for RenderedRunner { #[cfg(feature = "hydration")] Rendered::Hydration { .. } => {} - // We only call rendered when the component is rendered & not suspended.. + // We only call rendered when the component is rendered & not suspended. Rendered::Render { .. } => { if state.suspension.is_none() { state.inner.rendered(self.first_render); diff --git a/packages/yew/src/html/component/marker.rs b/packages/yew/src/html/component/marker.rs index b2f7a410e47..03263ee94ab 100644 --- a/packages/yew/src/html/component/marker.rs +++ b/packages/yew/src/html/component/marker.rs @@ -8,7 +8,7 @@ use crate::html::{BaseComponent, ChildrenProps, Component, Context, Html}; /// A Component to represent a component that does not exist in current implementation. /// /// During Hydration, Yew expected the Virtual DOM hierarchy to match the the layout used in server-side -/// renering. However, sometimes it is possible / reasonable to omit certain components from one +/// rendering. However, sometimes it is possible / reasonable to omit certain components from one /// side of the implementation. This component is used to represent a component as if a component "existed" /// in the place it is defined. /// diff --git a/packages/yew/src/html/component/scope.rs b/packages/yew/src/html/component/scope.rs index 35aa2b8c26b..39f2fd7d518 100644 --- a/packages/yew/src/html/component/scope.rs +++ b/packages/yew/src/html/component/scope.rs @@ -416,13 +416,10 @@ mod feat_ssr { if hydratable { #[cfg(debug_assertions)] - w.push_str(&format!( - "", - std::any::type_name::() - )); + w.push_str(&format!("", std::any::type_name::())); #[cfg(not(debug_assertions))] - w.push_str(""); + w.push_str(""); } let html = rx.await.unwrap(); @@ -432,13 +429,10 @@ mod feat_ssr { if hydratable { #[cfg(debug_assertions)] - w.push_str(&format!( - "", - std::any::type_name::() - )); + w.push_str(&format!("", std::any::type_name::())); #[cfg(not(debug_assertions))] - w.push_str(""); + w.push_str(""); } scheduler::push_component_destroy(DestroyRunner { @@ -553,7 +547,8 @@ mod feat_hydration { self.id ); - let fragment = Fragment::collect_between(fragment, &parent, "comp"); + let fragment = + Fragment::collect_between(fragment, &parent, "<[", "", "component"); node_ref.set(fragment.front().cloned()); let next_sibling = NodeRef::default(); diff --git a/packages/yew/src/virtual_dom/mod.rs b/packages/yew/src/virtual_dom/mod.rs index 9c5d96145b6..363a97af2d2 100644 --- a/packages/yew/src/virtual_dom/mod.rs +++ b/packages/yew/src/virtual_dom/mod.rs @@ -631,64 +631,68 @@ mod feat_hydration { pub(crate) fn collect_between( collect_from: &mut Fragment, parent: &Element, - divider: &str, + open_start_mark: &str, + close_start_mark: &str, + end_mark: &str, + kind_name: &str, ) -> Self { - let start_mark = format!("yew-{}-start", divider); - let end_mark = format!("yew-{}-end", divider); + let is_open_tag = |node: &Node| { + let comment_text = node.text_content().unwrap_or_else(|| "".to_string()); - // We trim all text nodes as it's likely these are whitespaces. + comment_text.starts_with(&open_start_mark) && comment_text.ends_with(&end_mark) + }; + + let is_close_tag = |node: &Node| { + let comment_text = node.text_content().unwrap_or_else(|| "".to_string()); + + comment_text.starts_with(&close_start_mark) && comment_text.ends_with(&end_mark) + }; + + // We trim all leading text nodes as it's likely these are whitespaces. collect_from.trim_start_text_nodes(parent); let first_node = collect_from .pop_front() - .unwrap_or_else(|| panic!("expected {} start, found EOF", divider)); + .unwrap_or_else(|| panic!("expected {} opening tag, found EOF", kind_name)); assert_eq!( first_node.node_type(), Node::COMMENT_NODE, // TODO: improve error message with human readable node type name. "expected {} start, found node type {}", - divider, + kind_name, first_node.node_type() ); - // We remove the start comment. - parent.remove_child(&first_node).unwrap(); - let mut nodes = VecDeque::new(); - if !first_node - .text_content() - .unwrap_or_else(|| "".to_string()) - .starts_with(&start_mark) - { - panic!("expected {} start, found comment node", divider); + if !is_open_tag(&first_node) { + panic!("expected {} opening tag, found comment node", kind_name); } + // We remove the opening tag. + parent.remove_child(&first_node).unwrap(); + let mut current_node; let mut nested_layers = 1; loop { current_node = collect_from .pop_front() - .unwrap_or_else(|| panic!("expected {} end, found EOF", divider)); + .unwrap_or_else(|| panic!("expected {} closing tag, found EOF", kind_name)); if current_node.node_type() == Node::COMMENT_NODE { - let text_content = current_node - .text_content() - .unwrap_or_else(|| "".to_string()); - - if text_content.starts_with(&start_mark) { - // We found another component, we need to increase component counter. + if is_open_tag(¤t_node) { + // We found another opening tag, we need to increase component counter. nested_layers += 1; - } else if text_content.starts_with(&end_mark) { - // We found a component end, minus component counter. + } else if is_close_tag(¤t_node) { + // We found a closing tag, minus component counter. nested_layers -= 1; if nested_layers == 0 { - // We have found the component end of the current component, breaking + // We have found the end of the current tag we are collecting, breaking // the loop. - // We remove the end comment. + // We remove the closing tag. parent.remove_child(¤t_node).unwrap(); break; } diff --git a/packages/yew/src/virtual_dom/vsuspense.rs b/packages/yew/src/virtual_dom/vsuspense.rs index 07d409c3af1..2318833b7ef 100644 --- a/packages/yew/src/virtual_dom/vsuspense.rs +++ b/packages/yew/src/virtual_dom/vsuspense.rs @@ -270,7 +270,8 @@ mod feat_hydration { // We start hydration with the VSuspense being suspended. // A subsequent render will resume the VSuspense if not needed to be suspended. - let fallback_nodes = Fragment::collect_between(fragment, parent, "suspense"); + let fallback_nodes = + Fragment::collect_between(fragment, parent, "", "suspense"); let mut nodes = fallback_nodes.deep_clone(); @@ -313,7 +314,7 @@ mod feat_ssr { hydratable: bool, ) { if hydratable { - w.push_str(""); + w.push_str(""); } // always render children on the server side. self.children @@ -321,7 +322,7 @@ mod feat_ssr { .await; if hydratable { - w.push_str(""); + w.push_str(""); } } } diff --git a/packages/yew/tests/hydration.rs b/packages/yew/tests/hydration.rs index eb698114a81..89e5b3508b5 100644 --- a/packages/yew/tests/hydration.rs +++ b/packages/yew/tests/hydration.rs @@ -190,7 +190,7 @@ async fn hydration_with_suspense() { // still hydrating, during hydration, the server rendered result is shown. assert_eq!( result.as_str(), - r#"
0
"# + r#"
0
"# ); sleep(Duration::from_millis(50)).await; @@ -475,7 +475,7 @@ async fn hydration_nested_suspense_works() { let result = obtain_result(); assert_eq!( result.as_str(), - r#"
"# + r#"
"# ); sleep(Duration::from_millis(50)).await; @@ -484,7 +484,7 @@ async fn hydration_nested_suspense_works() { let result = obtain_result(); assert_eq!( result.as_str(), - r#"
"# + r#"
"# ); sleep(Duration::from_millis(50)).await; From c231e6054bf9d772f02de9a0ea2e3d388ee4d334 Mon Sep 17 00:00:00 2001 From: Kaede Hoshikawa Date: Sun, 13 Feb 2022 21:32:02 +0900 Subject: [PATCH 37/40] Prevent fallback from suspending Suspense. --- packages/yew/src/html/component/lifecycle.rs | 6 +- packages/yew/src/suspense/component.rs | 92 +++++++++++++++----- packages/yew/src/suspense/mod.rs | 1 + packages/yew/src/virtual_dom/vsuspense.rs | 8 +- packages/yew/tests/hydration.rs | 2 +- 5 files changed, 79 insertions(+), 30 deletions(-) diff --git a/packages/yew/src/html/component/lifecycle.rs b/packages/yew/src/html/component/lifecycle.rs index 4659290bc1c..c3c295fe0b8 100644 --- a/packages/yew/src/html/component/lifecycle.rs +++ b/packages/yew/src/html/component/lifecycle.rs @@ -3,7 +3,7 @@ use super::{AnyScope, BaseComponent, Scope}; use crate::html::{RenderError, RenderResult}; use crate::scheduler::{self, Runnable, Shared}; -use crate::suspense::{Suspense, Suspension}; +use crate::suspense::{BaseSuspense, Suspension}; #[cfg(feature = "hydration")] use crate::virtual_dom::{Fragment, VHydrate}; use crate::virtual_dom::{VDiff, VNode}; @@ -362,7 +362,7 @@ impl RenderRunner { if let Some(m) = state.suspension.take() { let comp_scope = state.inner.any_scope(); - let suspense_scope = comp_scope.find_parent_scope::().unwrap(); + let suspense_scope = comp_scope.find_parent_scope::().unwrap(); let suspense = suspense_scope.get_component().unwrap(); suspense.resume(m); @@ -470,7 +470,7 @@ impl RenderRunner { let comp_scope = state.inner.any_scope(); let suspense_scope = comp_scope - .find_parent_scope::() + .find_parent_scope::() .expect("To suspend rendering, a component is required."); let suspense = suspense_scope.get_component().unwrap(); diff --git a/packages/yew/src/suspense/component.rs b/packages/yew/src/suspense/component.rs index 7ed85f83ddc..d0da4da9586 100644 --- a/packages/yew/src/suspense/component.rs +++ b/packages/yew/src/suspense/component.rs @@ -1,3 +1,4 @@ +use crate::html; use crate::html::{Children, Component, Context, Html, Properties, Scope}; use crate::virtual_dom::{VList, VNode, VSuspense}; @@ -6,37 +7,36 @@ use web_sys::Element; use super::Suspension; #[derive(Properties, PartialEq, Debug, Clone)] -pub struct SuspenseProps { - #[prop_or_default] +pub(crate) struct BaseSuspenseProps { pub children: Children, - #[prop_or_default] pub fallback: Html, + + pub suspendible: bool, } #[derive(Debug)] -pub enum SuspenseMsg { +pub(crate) enum BaseSuspenseMsg { Suspend(Suspension), Resume(Suspension), } -/// Suspend rendering and show a fallback UI until the underlying task completes. +/// The Implementation of Suspense Component. #[derive(Debug)] -pub struct Suspense { +pub(crate) struct BaseSuspense { link: Scope, suspensions: Vec, detached_parent: Option, } -impl Component for Suspense { - type Properties = SuspenseProps; - type Message = SuspenseMsg; +impl Component for BaseSuspense { + type Properties = BaseSuspenseProps; + type Message = BaseSuspenseMsg; fn create(ctx: &Context) -> Self { Self { link: ctx.link().clone(), suspensions: Vec::new(), - #[cfg(target_arch = "wasm32")] detached_parent: web_sys::window() .and_then(|m| m.document()) @@ -47,9 +47,14 @@ impl Component for Suspense { } } - fn update(&mut self, _ctx: &Context, msg: Self::Message) -> bool { + fn update(&mut self, ctx: &Context, msg: Self::Message) -> bool { match msg { Self::Message::Suspend(m) => { + assert!( + ctx.props().suspendible, + "You cannot suspend from a component rendered as a fallback." + ); + if m.resumed() { return false; } @@ -70,23 +75,66 @@ impl Component for Suspense { } fn view(&self, ctx: &Context) -> Html { - let SuspenseProps { children, fallback } = (*ctx.props()).clone(); - - let children = VNode::from(VList::with_children(children.into_iter().collect(), None)); - - let fallback = (!self.suspensions.is_empty()).then(|| fallback); - - let vsuspense = VSuspense::new(children, fallback, self.detached_parent.clone()); - VNode::from(vsuspense) + let BaseSuspenseProps { + children, fallback, .. + } = (*ctx.props()).clone(); + + if ctx.props().suspendible { + let children = VNode::from(VList::with_children(children.into_iter().collect(), None)); + let fallback = (!self.suspensions.is_empty()).then(|| fallback); + + let vsuspense = VSuspense::new(children, fallback, self.detached_parent.clone()); + VNode::from(vsuspense) + } else { + html! {<>{children}} + } } } -impl Suspense { +impl BaseSuspense { pub(crate) fn suspend(&self, s: Suspension) { - self.link.send_message(SuspenseMsg::Suspend(s)); + self.link.send_message(BaseSuspenseMsg::Suspend(s)); } pub(crate) fn resume(&self, s: Suspension) { - self.link.send_message(SuspenseMsg::Resume(s)); + self.link.send_message(BaseSuspenseMsg::Resume(s)); + } +} + +#[derive(Properties, PartialEq, Debug, Clone)] +pub struct SuspenseProps { + #[prop_or_default] + pub children: Children, + + #[prop_or_default] + pub fallback: Html, +} + +/// Suspend rendering and show a fallback UI until the underlying task completes. +#[derive(Debug)] +pub struct Suspense {} + +impl Component for Suspense { + type Properties = SuspenseProps; + type Message = (); + + fn create(_ctx: &Context) -> Self { + Self {} + } + + fn view(&self, ctx: &Context) -> Html { + let SuspenseProps { children, fallback } = ctx.props().clone(); + + let fallback = html! { + + {fallback} + + }; + + html! { + + {children} + + } } } diff --git a/packages/yew/src/suspense/mod.rs b/packages/yew/src/suspense/mod.rs index 617c263775f..e720539fa22 100644 --- a/packages/yew/src/suspense/mod.rs +++ b/packages/yew/src/suspense/mod.rs @@ -3,5 +3,6 @@ mod component; mod suspension; +pub(crate) use component::BaseSuspense; pub use component::Suspense; pub use suspension::{Suspension, SuspensionHandle, SuspensionResult}; diff --git a/packages/yew/src/virtual_dom/vsuspense.rs b/packages/yew/src/virtual_dom/vsuspense.rs index 2318833b7ef..02415c29c1b 100644 --- a/packages/yew/src/virtual_dom/vsuspense.rs +++ b/packages/yew/src/virtual_dom/vsuspense.rs @@ -132,7 +132,7 @@ impl VDiff for VSuspense { // When it's suspended, we render children into an element that is detached from the dom // tree while rendering fallback UI into the original place where children resides in. match (self.fallback.as_mut(), fallback_ancestor) { - // Currently Suspended, Continue to be Suspended. + // Currently suspended, continue to be suspended. (Some(fallback), Some(fallback_ancestor)) => { match (fallback, fallback_ancestor) { ( @@ -177,7 +177,7 @@ impl VDiff for VSuspense { } } - // Currently not Suspended, Continue to be not Suspended. + // Currently not suspended, continue to be not suspended. (None, None) => { self.children .apply(parent_scope, parent, next_sibling, children_ancestor) @@ -282,7 +282,7 @@ mod feat_hydration { self.children .hydrate(parent_scope, detached_parent, &mut nodes); - // We trim all text nodes before checking as it's likely these are whitespaces. + // We trim all leading text nodes before checking as it's likely these are whitespaces. nodes.trim_start_text_nodes(detached_parent); assert!(nodes.is_empty(), "expected end of suspense, found node."); @@ -316,7 +316,7 @@ mod feat_ssr { if hydratable { w.push_str(""); } - // always render children on the server side. + // always render children on the server side (for now). self.children .render_to_string(w, parent_scope, hydratable) .await; diff --git a/packages/yew/tests/hydration.rs b/packages/yew/tests/hydration.rs index 89e5b3508b5..0dca41a7106 100644 --- a/packages/yew/tests/hydration.rs +++ b/packages/yew/tests/hydration.rs @@ -475,7 +475,7 @@ async fn hydration_nested_suspense_works() { let result = obtain_result(); assert_eq!( result.as_str(), - r#"
"# + r#"
"# ); sleep(Duration::from_millis(50)).await; From 4d96d9b4e63d3a9115084190d300f8d54e7e4ed1 Mon Sep 17 00:00:00 2001 From: Kaede Hoshikawa Date: Fri, 18 Feb 2022 19:27:14 +0900 Subject: [PATCH 38/40] Update packages/yew/src/html/component/marker.rs Co-authored-by: Muhammad Hamza --- packages/yew/src/html/component/marker.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/yew/src/html/component/marker.rs b/packages/yew/src/html/component/marker.rs index 03263ee94ab..5b7d294db62 100644 --- a/packages/yew/src/html/component/marker.rs +++ b/packages/yew/src/html/component/marker.rs @@ -127,7 +127,7 @@ use crate::html::{BaseComponent, ChildrenProps, Component, Context, Html}; /// /// /// -/// // Hydration will success as the PhantomComponent in the server-side +/// // Hydration will succeed as the PhantomComponent in the server-side /// // implementation will represent a Provider4 component in this position. /// /// From 3c7c305abf0d4afe6b8ed51495bf00cf80224fbe Mon Sep 17 00:00:00 2001 From: Kaede Hoshikawa Date: Fri, 18 Feb 2022 21:42:55 +0900 Subject: [PATCH 39/40] remove log, add trace_hydration. --- packages/yew/Cargo.toml | 2 +- packages/yew/src/html/component/scope.rs | 4 ++-- packages/yew/src/lib.rs | 3 +++ 3 files changed, 6 insertions(+), 3 deletions(-) diff --git a/packages/yew/Cargo.toml b/packages/yew/Cargo.toml index fe512467c1b..bf693ae286d 100644 --- a/packages/yew/Cargo.toml +++ b/packages/yew/Cargo.toml @@ -26,7 +26,6 @@ slab = "0.4" wasm-bindgen = "0.2" yew-macro = { version = "^0.19.0", path = "../yew-macro" } thiserror = "1.0" -log = "0.4" futures = { version = "0.3", optional = true } html-escape = { version = "0.2.9", optional = true } @@ -85,6 +84,7 @@ wasm_test = ["ssr", "hydration"] wasm_bench = [] ssr = ["futures", "html-escape"] hydration = [] +trace_hydration = ["hydration"] default = [] [target.'cfg(not(target_arch = "wasm32"))'.dev-dependencies] diff --git a/packages/yew/src/html/component/scope.rs b/packages/yew/src/html/component/scope.rs index 39f2fd7d518..872501af7c5 100644 --- a/packages/yew/src/html/component/scope.rs +++ b/packages/yew/src/html/component/scope.rs @@ -540,8 +540,8 @@ mod feat_hydration { // This is very helpful to see which component is failing during hydration // which means this component may not having a stable layout / differs between // client-side and server-side. - #[cfg(debug_assertions)] - log::trace!( + #[cfg(all(debug_assertions, feature = "trace_hydration"))] + gloo::console::trace!( "queuing hydration of: {}(ID: {})", std::any::type_name::(), self.id diff --git a/packages/yew/src/lib.rs b/packages/yew/src/lib.rs index 154d33276ac..c134d6f8386 100644 --- a/packages/yew/src/lib.rs +++ b/packages/yew/src/lib.rs @@ -22,6 +22,9 @@ //! - `tokio`: Enables future-based APIs on non-wasm32 targets with tokio runtime. (You may want to //! enable this if your application uses future-based APIs and it does not compile / lint on //! non-wasm32 targets.) +//! - `hydration`: Enables Hydration support. +//! - `trace_hydration`: Enables trace logging on hydration. (Implies `hydration`. You may want to enable this if you are +//! trying to debug hydration layout mismatch.) //! //! ## Example //! From b4a261626062b55257a5892ac223126a176ac922 Mon Sep 17 00:00:00 2001 From: Kaede Hoshikawa Date: Sat, 19 Feb 2022 22:51:18 +0900 Subject: [PATCH 40/40] Do not spawn a new thread for every request in the examples. --- examples/simple_ssr/Cargo.toml | 3 ++ .../simple_ssr/src/bin/simple_ssr_server.rs | 25 +++++++------- examples/ssr_router/Cargo.toml | 3 ++ .../ssr_router/src/bin/ssr_router_server.rs | 33 +++++++++---------- 4 files changed, 32 insertions(+), 32 deletions(-) diff --git a/examples/simple_ssr/Cargo.toml b/examples/simple_ssr/Cargo.toml index 92a17b92b64..50f879f0c5c 100644 --- a/examples/simple_ssr/Cargo.toml +++ b/examples/simple_ssr/Cargo.toml @@ -20,3 +20,6 @@ log = "0.4" tokio = { version = "1.15.0", features = ["full"] } warp = "0.3" structopt = "0.3" +num_cpus = "1.13" +tokio-util = { version = "0.7", features = ["rt"] } +once_cell = "1.5" diff --git a/examples/simple_ssr/src/bin/simple_ssr_server.rs b/examples/simple_ssr/src/bin/simple_ssr_server.rs index 99fa3ec5288..17a20d17cae 100644 --- a/examples/simple_ssr/src/bin/simple_ssr_server.rs +++ b/examples/simple_ssr/src/bin/simple_ssr_server.rs @@ -1,10 +1,13 @@ +use once_cell::sync::Lazy; use simple_ssr::App; use std::path::PathBuf; use structopt::StructOpt; -use tokio::task::spawn_blocking; -use tokio::task::LocalSet; +use tokio_util::task::LocalPoolHandle; use warp::Filter; +// We spawn a local pool that is as big as the number of cpu threads. +static LOCAL_POOL: Lazy = Lazy::new(|| LocalPoolHandle::new(num_cpus::get())); + /// A basic example #[derive(StructOpt, Debug)] struct Opt { @@ -14,23 +17,17 @@ struct Opt { } async fn render(index_html_s: &str) -> String { - let content = spawn_blocking(move || { - use tokio::runtime::Builder; - let set = LocalSet::new(); - - let rt = Builder::new_current_thread().enable_all().build().unwrap(); - - set.block_on(&rt, async { + let content = LOCAL_POOL + .spawn_pinned(move || async move { let renderer = yew::ServerRenderer::::new(); renderer.render().await }) - }) - .await - .expect("the thread has failed."); + .await + .expect("the task has failed."); - // Good enough for an example, but developers should print their html properly in actual - // application. + // Good enough for an example, but developers should avoid the replace and extra allocation + // here in an actual app. index_html_s.replace("", &format!("{}", content)) } diff --git a/examples/ssr_router/Cargo.toml b/examples/ssr_router/Cargo.toml index 933a0c76e7f..b6873c9d468 100644 --- a/examples/ssr_router/Cargo.toml +++ b/examples/ssr_router/Cargo.toml @@ -19,3 +19,6 @@ tokio = { version = "1.15.0", features = ["full"] } warp = "0.3" structopt = "0.3" env_logger = "0.9" +num_cpus = "1.13" +tokio-util = { version = "0.7", features = ["rt"] } +once_cell = "1.5" diff --git a/examples/ssr_router/src/bin/ssr_router_server.rs b/examples/ssr_router/src/bin/ssr_router_server.rs index f0e19172896..fcea83c105e 100644 --- a/examples/ssr_router/src/bin/ssr_router_server.rs +++ b/examples/ssr_router/src/bin/ssr_router_server.rs @@ -1,11 +1,14 @@ use function_router::{ServerApp, ServerAppProps}; +use once_cell::sync::Lazy; use std::collections::HashMap; use std::path::PathBuf; use structopt::StructOpt; -use tokio::task::spawn_blocking; -use tokio::task::LocalSet; +use tokio_util::task::LocalPoolHandle; use warp::Filter; +// We spawn a local pool that is as big as the number of cpu threads. +static LOCAL_POOL: Lazy = Lazy::new(|| LocalPoolHandle::new(num_cpus::get())); + /// A basic example #[derive(StructOpt, Debug)] struct Opt { @@ -17,28 +20,22 @@ struct Opt { async fn render(index_html_s: &str, url: &str, queries: HashMap) -> String { let url = url.to_string(); - let content = spawn_blocking(move || { - use tokio::runtime::Builder; - let set = LocalSet::new(); - - let rt = Builder::new_current_thread().enable_all().build().unwrap(); + let content = LOCAL_POOL + .spawn_pinned(move || async move { + let server_app_props = ServerAppProps { + url: url.into(), + queries, + }; - let server_app_props = ServerAppProps { - url: url.into(), - queries, - }; - - set.block_on(&rt, async { let renderer = yew::ServerRenderer::::with_props(server_app_props); renderer.render().await }) - }) - .await - .expect("the thread has failed."); + .await + .expect("the task has failed."); - // Good enough for an example, but developers should print their html properly in actual - // application. + // Good enough for an example, but developers should avoid the replace and extra allocation + // here in an actual app. index_html_s.replace("", &format!("{}", content)) }