diff --git a/.github/workflows/main-checks.yml b/.github/workflows/main-checks.yml
index 2a1ccce5592..8794ef1bd3c 100644
--- a/.github/workflows/main-checks.yml
+++ b/.github/workflows/main-checks.yml
@@ -32,6 +32,7 @@ jobs:
cargo clippy -- --deny=warnings
cargo clippy --features=ssr -- --deny=warnings
cargo clippy --features=csr -- --deny=warnings
+ cargo clippy --features=hydration -- --deny=warnings
cargo clippy --all-features --all-targets -- --deny=warnings
working-directory: packages/yew
@@ -62,6 +63,7 @@ jobs:
cargo clippy --release -- --deny=warnings
cargo clippy --release --features=ssr -- --deny=warnings
cargo clippy --release --features=csr -- --deny=warnings
+ cargo clippy --release --features=hydration -- --deny=warnings
cargo clippy --release --all-features --all-targets -- --deny=warnings
working-directory: packages/yew
diff --git a/Cargo.toml b/Cargo.toml
index 3fc5039774f..527240dd828 100644
--- a/Cargo.toml
+++ b/Cargo.toml
@@ -34,6 +34,7 @@ members = [
"examples/two_apps",
"examples/webgl",
"examples/web_worker_fib",
+ "examples/ssr_router",
"examples/suspense",
# Tools
diff --git a/examples/function_router/Cargo.toml b/examples/function_router/Cargo.toml
index 76258d84570..9bc73d082da 100644
--- a/examples/function_router/Cargo.toml
+++ b/examples/function_router/Cargo.toml
@@ -13,14 +13,11 @@ yew-router = { path = "../../packages/yew-router" }
serde = { version = "1.0", features = ["derive"] }
lazy_static = "1.4.0"
gloo-timers = "0.2"
+wasm-logger = "0.2"
+instant = { version = "0.1", features = ["wasm-bindgen"] }
[target.'cfg(target_arch = "wasm32")'.dependencies]
getrandom = { version = "0.2", features = ["js"] }
-instant = { version = "0.1", features = ["wasm-bindgen"] }
-wasm-logger = "0.2"
-
-[target.'cfg(not(target_arch = "wasm32"))'.dependencies]
-instant = { version = "0.1" }
[features]
csr = ["yew/csr"]
diff --git a/examples/function_router/index.html b/examples/function_router/index.html
index 2c5aee2ff8f..2a75b730809 100644
--- a/examples/function_router/index.html
+++ b/examples/function_router/index.html
@@ -11,7 +11,7 @@
href="https://cdn.jsdelivr.net/npm/bulma@0.9.0/css/bulma.min.css"
/>
-
+
diff --git a/examples/function_router/src/app.rs b/examples/function_router/src/app.rs
index ce581a59bf4..4eac347d887 100644
--- a/examples/function_router/src/app.rs
+++ b/examples/function_router/src/app.rs
@@ -1,4 +1,8 @@
+use std::collections::HashMap;
+
use yew::prelude::*;
+use yew::virtual_dom::AttrValue;
+use yew_router::history::{AnyHistory, History, MemoryHistory};
use yew_router::prelude::*;
use crate::components::nav::Nav;
@@ -47,53 +51,40 @@ pub fn App() -> Html {
}
}
-#[cfg(not(target_arch = "wasm32"))]
-mod arch_native {
- use super::*;
-
- use yew::virtual_dom::AttrValue;
- use yew_router::history::{AnyHistory, History, MemoryHistory};
-
- use std::collections::HashMap;
-
- #[derive(Properties, PartialEq, Debug)]
- pub struct ServerAppProps {
- pub url: AttrValue,
- pub queries: HashMap,
- }
+#[derive(Properties, PartialEq, Debug)]
+pub struct ServerAppProps {
+ pub url: AttrValue,
+ pub queries: HashMap,
+}
- #[function_component]
- pub fn ServerApp(props: &ServerAppProps) -> Html {
- let history = AnyHistory::from(MemoryHistory::new());
- history
- .push_with_query(&*props.url, &props.queries)
- .unwrap();
+#[function_component]
+pub fn ServerApp(props: &ServerAppProps) -> Html {
+ let history = AnyHistory::from(MemoryHistory::new());
+ history
+ .push_with_query(&*props.url, &props.queries)
+ .unwrap();
- html! {
-
-
+ html! {
+
+
-
- render={Switch::render(switch)} />
-
-
-
- }
+
+ render={Switch::render(switch)} />
+
+
+
}
}
-#[cfg(not(target_arch = "wasm32"))]
-pub use arch_native::*;
-
fn switch(routes: &Route) -> Html {
match routes.clone() {
Route::Post { id } => {
diff --git a/examples/function_router/src/bin/function_router.rs b/examples/function_router/src/bin/function_router.rs
new file mode 100644
index 00000000000..5f5017765d8
--- /dev/null
+++ b/examples/function_router/src/bin/function_router.rs
@@ -0,0 +1,7 @@
+pub use function_router::*;
+
+fn main() {
+ wasm_logger::init(wasm_logger::Config::new(log::Level::Trace));
+ #[cfg(feature = "csr")]
+ yew::Renderer::::new().render();
+}
diff --git a/examples/function_router/src/main.rs b/examples/function_router/src/main.rs
deleted file mode 100644
index ee2be1e7df0..00000000000
--- a/examples/function_router/src/main.rs
+++ /dev/null
@@ -1,14 +0,0 @@
-mod app;
-mod components;
-mod content;
-mod generator;
-mod pages;
-
-pub use app::*;
-
-fn main() {
- #[cfg(target_arch = "wasm32")]
- wasm_logger::init(wasm_logger::Config::new(log::Level::Trace));
- #[cfg(feature = "render")]
- yew::Renderer::::new().render();
-}
diff --git a/examples/simple_ssr/Cargo.toml b/examples/simple_ssr/Cargo.toml
index d4812fcc79b..0a0e2b47bec 100644
--- a/examples/simple_ssr/Cargo.toml
+++ b/examples/simple_ssr/Cargo.toml
@@ -6,9 +6,20 @@ edition = "2021"
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
[dependencies]
-tokio = { version = "1.15.0", features = ["full"] }
-warp = "0.3"
-yew = { path = "../../packages/yew", features = ["ssr"] }
+yew = { path = "../../packages/yew", features = ["ssr", "hydration"] }
reqwest = { version = "0.11.8", features = ["json"] }
serde = { version = "1.0.132", features = ["derive"] }
uuid = { version = "0.8.2", features = ["serde"] }
+
+[target.'cfg(target_arch = "wasm32")'.dependencies]
+wasm-bindgen-futures = "0.4"
+wasm-logger = "0.2"
+log = "0.4"
+
+[target.'cfg(not(target_arch = "wasm32"))'.dependencies]
+tokio = { version = "1.15.0", features = ["full"] }
+warp = "0.3"
+num_cpus = "1.13"
+tokio-util = { version = "0.7", features = ["rt"] }
+once_cell = "1.5"
+clap = { version = "3.1.7", features = ["derive"] }
diff --git a/examples/simple_ssr/README.md b/examples/simple_ssr/README.md
index 95cf18b43ea..6c02a63edf3 100644
--- a/examples/simple_ssr/README.md
+++ b/examples/simple_ssr/README.md
@@ -2,5 +2,16 @@
This example demonstrates server-side rendering.
-Run `cargo run -p simple_ssr` and navigate to http://localhost:8080/ to
-view results.
+# How to run this example
+
+1. build hydration bundle
+
+`trunk build examples/simple_ssr/index.html`
+
+2. Run the server
+
+`cargo run --bin simple_ssr_server -- --dir examples/simple_ssr/dist`
+
+3. Open Browser
+
+Navigate to http://localhost:8080/ to view results.
diff --git a/examples/simple_ssr/index.html b/examples/simple_ssr/index.html
new file mode 100644
index 00000000000..62951cf4073
--- /dev/null
+++ b/examples/simple_ssr/index.html
@@ -0,0 +1,9 @@
+
+
+
+
+ Yew SSR Example
+
+
+
+
diff --git a/examples/simple_ssr/src/bin/simple_ssr_hydrate.rs b/examples/simple_ssr/src/bin/simple_ssr_hydrate.rs
new file mode 100644
index 00000000000..8634be81faf
--- /dev/null
+++ b/examples/simple_ssr/src/bin/simple_ssr_hydrate.rs
@@ -0,0 +1,7 @@
+use simple_ssr::App;
+
+fn main() {
+ #[cfg(target_arch = "wasm32")]
+ wasm_logger::init(wasm_logger::Config::new(log::Level::Trace));
+ yew::Renderer::::new().hydrate();
+}
diff --git a/examples/simple_ssr/src/bin/simple_ssr_server.rs b/examples/simple_ssr/src/bin/simple_ssr_server.rs
new file mode 100644
index 00000000000..86b062081fd
--- /dev/null
+++ b/examples/simple_ssr/src/bin/simple_ssr_server.rs
@@ -0,0 +1,53 @@
+use clap::Parser;
+use once_cell::sync::Lazy;
+use simple_ssr::App;
+use std::path::PathBuf;
+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(Parser, Debug)]
+struct Opt {
+ /// the "dist" created by trunk directory to be served for hydration.
+ #[structopt(short, long, parse(from_os_str))]
+ dir: PathBuf,
+}
+
+async fn render(index_html_s: &str) -> String {
+ let content = LOCAL_POOL
+ .spawn_pinned(move || async move {
+ let renderer = yew::ServerRenderer::::new();
+
+ renderer.render().await
+ })
+ .await
+ .expect("the task has failed.");
+
+ // 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))
+}
+
+#[tokio::main]
+async fn main() {
+ let opts = Opt::parse();
+
+ let index_html_s = tokio::fs::read_to_string(opts.dir.join("index.html"))
+ .await
+ .expect("failed to read index.html");
+
+ let html = warp::path::end().then(move || {
+ let index_html_s = index_html_s.clone();
+
+ async move { warp::reply::html(render(&index_html_s).await) }
+ });
+
+ let routes = html.or(warp::fs::dir(opts.dir));
+
+ println!("You can view the website at: http://localhost:8080/");
+
+ warp::serve(routes).run(([127, 0, 0, 1], 8080)).await;
+}
diff --git a/examples/simple_ssr/src/main.rs b/examples/simple_ssr/src/lib.rs
similarity index 64%
rename from examples/simple_ssr/src/main.rs
rename to examples/simple_ssr/src/lib.rs
index 58dbb0dda8d..ad7af2e0bba 100644
--- a/examples/simple_ssr/src/main.rs
+++ b/examples/simple_ssr/src/lib.rs
@@ -2,13 +2,15 @@ use std::cell::RefCell;
use std::rc::Rc;
use serde::{Deserialize, Serialize};
-use tokio::task::LocalSet;
-use tokio::task::{spawn_blocking, spawn_local};
use uuid::Uuid;
-use warp::Filter;
use yew::prelude::*;
use yew::suspense::{Suspension, SuspensionResult};
+#[cfg(not(target_arch = "wasm32"))]
+use tokio::task::spawn_local;
+#[cfg(target_arch = "wasm32")]
+use wasm_bindgen_futures::spawn_local;
+
#[derive(Serialize, Deserialize)]
struct UuidResponse {
uuid: Uuid,
@@ -79,7 +81,7 @@ fn Content() -> HtmlResult {
}
#[function_component]
-fn App() -> Html {
+pub fn App() -> Html {
let fallback = html! {{"Loading..."}
};
html! {
@@ -88,43 +90,3 @@ fn App() -> Html {
}
}
-
-async fn render() -> 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 renderer = yew::ServerRenderer::::new();
-
- renderer.render().await
- })
- })
- .await
- .expect("the thread has failed.");
-
- format!(
- r#"
-
-
- Yew SSR Example
-
-
- {}
-
-
-"#,
- content
- )
-}
-
-#[tokio::main]
-async fn main() {
- let routes = warp::any().then(|| async move { warp::reply::html(render().await) });
-
- println!("You can view the website at: http://localhost:8080/");
-
- warp::serve(routes).run(([127, 0, 0, 1], 8080)).await;
-}
diff --git a/examples/ssr_router/Cargo.toml b/examples/ssr_router/Cargo.toml
new file mode 100644
index 00000000000..49092a942f0
--- /dev/null
+++ b/examples/ssr_router/Cargo.toml
@@ -0,0 +1,24 @@
+[package]
+name = "ssr_router"
+version = "0.1.0"
+edition = "2021"
+
+# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
+
+[dependencies]
+yew = { path = "../../packages/yew", features = ["ssr", "hydration", "trace_hydration"] }
+function_router = { path = "../function_router" }
+log = "0.4"
+
+[target.'cfg(target_arch = "wasm32")'.dependencies]
+wasm-bindgen-futures = "0.4"
+wasm-logger = "0.2"
+
+[target.'cfg(not(target_arch = "wasm32"))'.dependencies]
+tokio = { version = "1.15.0", features = ["full"] }
+warp = "0.3"
+env_logger = "0.9"
+num_cpus = "1.13"
+tokio-util = { version = "0.7", features = ["rt"] }
+once_cell = "1.5"
+clap = { version = "3.1.7", features = ["derive"] }
diff --git a/examples/ssr_router/README.md b/examples/ssr_router/README.md
new file mode 100644
index 00000000000..0f65d5a50b9
--- /dev/null
+++ b/examples/ssr_router/README.md
@@ -0,0 +1,19 @@
+# SSR Router Example
+
+This example is the same as the function router example, but with
+server-side rendering and hydration support. It reuses the same codebase
+of the function router example.
+
+# How to run this example
+
+1. Build Hydration Bundle
+
+`trunk build examples/ssr_router/index.html`
+
+2. Run the server
+
+`cargo run --bin ssr_router_server -- --dir examples/ssr_router/dist`
+
+3. Open Browser
+
+Navigate to http://localhost:8080/ to view results.
diff --git a/examples/ssr_router/index.html b/examples/ssr_router/index.html
new file mode 100644
index 00000000000..95eb7d33dd4
--- /dev/null
+++ b/examples/ssr_router/index.html
@@ -0,0 +1,15 @@
+
+
+
+
+
+
+
+ Yew • SSR Router
+
+
+
+
diff --git a/examples/ssr_router/src/bin/ssr_router_hydrate.rs b/examples/ssr_router/src/bin/ssr_router_hydrate.rs
new file mode 100644
index 00000000000..7de78d81c10
--- /dev/null
+++ b/examples/ssr_router/src/bin/ssr_router_hydrate.rs
@@ -0,0 +1,7 @@
+use function_router::App;
+
+fn main() {
+ #[cfg(target_arch = "wasm32")]
+ wasm_logger::init(wasm_logger::Config::new(log::Level::Trace));
+ yew::Renderer::::new().hydrate();
+}
diff --git a/examples/ssr_router/src/bin/ssr_router_server.rs b/examples/ssr_router/src/bin/ssr_router_server.rs
new file mode 100644
index 00000000000..d23a0e02198
--- /dev/null
+++ b/examples/ssr_router/src/bin/ssr_router_server.rs
@@ -0,0 +1,71 @@
+use clap::Parser;
+use function_router::{ServerApp, ServerAppProps};
+use once_cell::sync::Lazy;
+use std::collections::HashMap;
+use std::path::PathBuf;
+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(Parser, Debug)]
+struct Opt {
+ /// the "dist" created by trunk directory to be served for hydration.
+ #[structopt(short, long, parse(from_os_str))]
+ dir: PathBuf,
+}
+
+async fn render(index_html_s: &str, url: &str, queries: HashMap) -> String {
+ let url = url.to_string();
+
+ let content = LOCAL_POOL
+ .spawn_pinned(move || async move {
+ let server_app_props = ServerAppProps {
+ url: url.into(),
+ queries,
+ };
+
+ let renderer = yew::ServerRenderer::::with_props(server_app_props);
+
+ renderer.render().await
+ })
+ .await
+ .expect("the task has failed.");
+
+ // 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))
+}
+
+#[tokio::main]
+async fn main() {
+ env_logger::init();
+
+ let opts = Opt::parse();
+
+ let index_html_s = tokio::fs::read_to_string(opts.dir.join("index.html"))
+ .await
+ .expect("failed to read index.html");
+
+ let render = move |s: warp::filters::path::FullPath, queries: HashMap| {
+ let index_html_s = index_html_s.clone();
+
+ async move { warp::reply::html(render(&index_html_s, s.as_str(), queries).await) }
+ };
+
+ let html = warp::path::end().and(
+ warp::path::full()
+ .and(warp::filters::query::query())
+ .then(render.clone()),
+ );
+
+ let routes = html.or(warp::fs::dir(opts.dir)).or(warp::path::full()
+ .and(warp::filters::query::query())
+ .then(render));
+
+ println!("You can view the website at: http://localhost:8080/");
+
+ warp::serve(routes).run(([127, 0, 0, 1], 8080)).await;
+}
diff --git a/examples/ssr_router/src/lib.rs b/examples/ssr_router/src/lib.rs
new file mode 100644
index 00000000000..8b137891791
--- /dev/null
+++ b/examples/ssr_router/src/lib.rs
@@ -0,0 +1 @@
+
diff --git a/packages/yew-router/src/router.rs b/packages/yew-router/src/router.rs
index add8d80e4de..fa32a77fa76 100644
--- a/packages/yew-router/src/router.rs
+++ b/packages/yew-router/src/router.rs
@@ -58,15 +58,12 @@ impl NavigatorContext {
}
}
-/// The Router component.
-///
-/// This provides location and navigator context to its children and switches.
+/// The base router.
///
-/// If you are building a web application, you may want to consider using [`BrowserRouter`] instead.
-///
-/// You only need one ` ` for each application.
-#[function_component(Router)]
-pub fn router(props: &RouterProps) -> Html {
+/// The implementation is separated to make sure has the same virtual dom layout as
+/// the and .
+#[function_component(BaseRouter)]
+fn base_router(props: &RouterProps) -> Html {
let RouterProps {
history,
children,
@@ -117,6 +114,20 @@ pub fn router(props: &RouterProps) -> Html {
}
}
+/// The Router component.
+///
+/// This provides location and navigator context to its children and switches.
+///
+/// If you are building a web application, you may want to consider using [`BrowserRouter`] instead.
+///
+/// You only need one ` ` for each application.
+#[function_component(Router)]
+pub fn router(props: &RouterProps) -> Html {
+ html! {
+
+ }
+}
+
/// Props for [`BrowserRouter`] and [`HashRouter`].
#[derive(Properties, PartialEq, Clone)]
pub struct ConcreteRouterProps {
@@ -143,9 +154,9 @@ pub fn browser_router(props: &ConcreteRouterProps) -> Html {
let basename = basename.map(|m| m.to_string()).or_else(base_url);
html! {
-
+
{children}
-
+
}
}
@@ -163,8 +174,8 @@ pub fn hash_router(props: &ConcreteRouterProps) -> Html {
let history = use_state(|| AnyHistory::from(HashHistory::new()));
html! {
-
+
{children}
-
+
}
}
diff --git a/packages/yew-router/src/utils.rs b/packages/yew-router/src/utils.rs
index d4a7d472dfe..38c8beaa379 100644
--- a/packages/yew-router/src/utils.rs
+++ b/packages/yew-router/src/utils.rs
@@ -41,6 +41,7 @@ pub fn fetch_base_url() -> Option {
}
}
+#[cfg(target_arch = "wasm32")]
pub fn compose_path(pathname: &str, query: &str) -> Option {
gloo::utils::window()
.location()
@@ -53,6 +54,17 @@ pub fn compose_path(pathname: &str, query: &str) -> Option {
})
}
+#[cfg(not(target_arch = "wasm32"))]
+pub fn compose_path(pathname: &str, query: &str) -> Option {
+ let query = query.trim();
+
+ if !query.is_empty() {
+ Some(format!("{}?{}", pathname, query))
+ } else {
+ Some(pathname.to_owned())
+ }
+}
+
#[cfg(test)]
mod tests {
use gloo::utils::document;
diff --git a/packages/yew/Cargo.toml b/packages/yew/Cargo.toml
index 575845f7c49..5c29f817b54 100644
--- a/packages/yew/Cargo.toml
+++ b/packages/yew/Cargo.toml
@@ -51,6 +51,7 @@ features = [
"Location",
"MouseEvent",
"Node",
+ "NodeList",
"PointerEvent",
"ProgressEvent",
"Text",
@@ -87,8 +88,10 @@ features = [
[features]
ssr = ["futures", "html-escape"]
csr = []
-doc_test = ["csr", "ssr"]
-wasm_test = ["csr"]
+hydration = ["csr"]
+trace_hydration = ["hydration"]
+doc_test = ["csr", "hydration", "ssr"]
+wasm_test = ["csr", "hydration", "ssr"]
default = []
[target.'cfg(not(target_arch = "wasm32"))'.dev-dependencies]
diff --git a/packages/yew/Makefile.toml b/packages/yew/Makefile.toml
index bf44c39d317..c08f61944a9 100644
--- a/packages/yew/Makefile.toml
+++ b/packages/yew/Makefile.toml
@@ -39,11 +39,13 @@ set -ex
cargo clippy -- --deny=warnings
cargo clippy --features=ssr -- --deny=warnings
cargo clippy --features=csr -- --deny=warnings
+cargo clippy --features=hydration -- --deny=warnings
cargo clippy --all-features --all-targets -- --deny=warnings
cargo clippy --release -- --deny=warnings
cargo clippy --release --features=ssr -- --deny=warnings
cargo clippy --release --features=csr -- --deny=warnings
+cargo clippy --release --features=hydration -- --deny=warnings
cargo clippy --release --all-features --all-targets -- --deny=warnings
'''
diff --git a/packages/yew/src/app_handle.rs b/packages/yew/src/app_handle.rs
index 06ac4501263..b741bec52a9 100644
--- a/packages/yew/src/app_handle.rs
+++ b/packages/yew/src/app_handle.rs
@@ -63,3 +63,41 @@ fn clear_element(host: &Element) {
host.remove_child(&child).expect("can't remove a child");
}
}
+
+#[cfg_attr(documenting, doc(cfg(feature = "hydration")))]
+#[cfg(feature = "hydration")]
+mod feat_hydration {
+ use super::*;
+
+ use crate::dom_bundle::Fragment;
+
+ impl AppHandle
+ where
+ ICOMP: IntoComponent,
+ {
+ pub(crate) fn hydrate_with_props(host: Element, props: Rc) -> Self {
+ let app = Self {
+ scope: Scope::new(None),
+ };
+
+ let mut fragment = Fragment::collect_children(&host);
+ let hosting_root = BSubtree::create_root(&host);
+
+ app.scope.hydrate_in_place(
+ hosting_root,
+ host.clone(),
+ &mut fragment,
+ NodeRef::default(),
+ props,
+ );
+
+ // We remove all remaining nodes, this mimics the clear_element behaviour in
+ // mount_with_props.
+ for node in fragment.iter() {
+ host.remove_child(node).unwrap();
+ }
+
+ app
+ }
+ }
+}
diff --git a/packages/yew/src/dom_bundle/bcomp.rs b/packages/yew/src/dom_bundle/bcomp.rs
index d23b90b0adf..b47c24ff579 100644
--- a/packages/yew/src/dom_bundle/bcomp.rs
+++ b/packages/yew/src/dom_bundle/bcomp.rs
@@ -119,6 +119,48 @@ impl Reconcilable for VComp {
}
}
+#[cfg(feature = "hydration")]
+mod feat_hydration {
+ use super::*;
+
+ use crate::dom_bundle::{Fragment, Hydratable};
+
+ impl Hydratable for VComp {
+ fn hydrate(
+ self,
+ root: &BSubtree,
+ parent_scope: &AnyScope,
+ parent: &Element,
+ fragment: &mut Fragment,
+ ) -> (NodeRef, Self::Bundle) {
+ let VComp {
+ type_id,
+ mountable,
+ node_ref,
+ key,
+ } = self;
+
+ let scoped = mountable.hydrate(
+ root.clone(),
+ parent_scope,
+ parent.clone(),
+ fragment,
+ node_ref.clone(),
+ );
+
+ (
+ node_ref.clone(),
+ BComp {
+ type_id,
+ scope: scoped,
+ node_ref,
+ key,
+ },
+ )
+ }
+ }
+}
+
#[cfg(feature = "wasm_test")]
#[cfg(test)]
mod tests {
diff --git a/packages/yew/src/dom_bundle/blist.rs b/packages/yew/src/dom_bundle/blist.rs
index fc0caf9201a..ce6c0878d28 100644
--- a/packages/yew/src/dom_bundle/blist.rs
+++ b/packages/yew/src/dom_bundle/blist.rs
@@ -453,6 +453,47 @@ impl Reconcilable for VList {
}
}
+#[cfg(feature = "hydration")]
+mod feat_hydration {
+ use super::*;
+
+ use crate::dom_bundle::{Fragment, Hydratable};
+
+ impl Hydratable for VList {
+ fn hydrate(
+ self,
+ root: &BSubtree,
+ parent_scope: &AnyScope,
+ parent: &Element,
+ fragment: &mut Fragment,
+ ) -> (NodeRef, Self::Bundle) {
+ let node_ref = NodeRef::default();
+ let mut children = Vec::with_capacity(self.children.len());
+
+ for (index, child) in self.children.into_iter().enumerate() {
+ let (child_node_ref, child) = child.hydrate(root, parent_scope, parent, fragment);
+
+ if index == 0 {
+ node_ref.reuse(child_node_ref);
+ }
+
+ children.push(child);
+ }
+
+ children.reverse();
+
+ (
+ node_ref,
+ BList {
+ rev_children: children,
+ fully_keyed: self.fully_keyed,
+ key: self.key,
+ },
+ )
+ }
+ }
+}
+
#[cfg(test)]
mod layout_tests {
extern crate self as yew;
diff --git a/packages/yew/src/dom_bundle/bnode.rs b/packages/yew/src/dom_bundle/bnode.rs
index f04928da189..79586b70361 100644
--- a/packages/yew/src/dom_bundle/bnode.rs
+++ b/packages/yew/src/dom_bundle/bnode.rs
@@ -233,6 +233,55 @@ impl fmt::Debug for BNode {
}
}
+#[cfg(feature = "hydration")]
+mod feat_hydration {
+ use super::*;
+
+ use crate::dom_bundle::{Fragment, Hydratable};
+
+ impl Hydratable for VNode {
+ fn hydrate(
+ self,
+ root: &BSubtree,
+ parent_scope: &AnyScope,
+ parent: &Element,
+ fragment: &mut Fragment,
+ ) -> (NodeRef, Self::Bundle) {
+ match self {
+ VNode::VTag(vtag) => {
+ let (node_ref, tag) = vtag.hydrate(root, parent_scope, parent, fragment);
+ (node_ref, tag.into())
+ }
+ VNode::VText(vtext) => {
+ let (node_ref, text) = vtext.hydrate(root, parent_scope, parent, fragment);
+ (node_ref, text.into())
+ }
+ VNode::VComp(vcomp) => {
+ let (node_ref, comp) = vcomp.hydrate(root, parent_scope, parent, fragment);
+ (node_ref, comp.into())
+ }
+ VNode::VList(vlist) => {
+ let (node_ref, list) = vlist.hydrate(root, parent_scope, parent, fragment);
+ (node_ref, list.into())
+ }
+ // You cannot hydrate a VRef.
+ VNode::VRef(_) => {
+ panic!("VRef is not hydratable. Try moving it to a component mounted after an effect.")
+ }
+ // You cannot hydrate a VPortal.
+ VNode::VPortal(_) => {
+ panic!("VPortal is not hydratable. Try creating your portal by delaying it with use_effect.")
+ }
+ VNode::VSuspense(vsuspense) => {
+ let (node_ref, suspense) =
+ vsuspense.hydrate(root, parent_scope, parent, fragment);
+ (node_ref, suspense.into())
+ }
+ }
+ }
+ }
+}
+
#[cfg(test)]
mod layout_tests {
use super::*;
diff --git a/packages/yew/src/dom_bundle/bsuspense.rs b/packages/yew/src/dom_bundle/bsuspense.rs
index d653640d476..8be6af8afb9 100644
--- a/packages/yew/src/dom_bundle/bsuspense.rs
+++ b/packages/yew/src/dom_bundle/bsuspense.rs
@@ -7,12 +7,24 @@ use crate::NodeRef;
use gloo::utils::document;
use web_sys::Element;
+#[cfg(feature = "hydration")]
+use super::Fragment;
+
+#[derive(Debug)]
+enum Fallback {
+ /// Suspense Fallback with fallback being rendered as placeholder.
+ Bundle(BNode),
+ /// Suspense Fallback with Hydration Fragment being rendered as placeholder.
+ #[cfg(feature = "hydration")]
+ Fragment(Fragment),
+}
+
/// The bundle implementation to [VSuspense]
#[derive(Debug)]
pub(super) struct BSuspense {
children_bundle: BNode,
/// The supsense is suspended if fallback contains [Some] bundle
- fallback_bundle: Option,
+ fallback: Option,
detached_parent: Element,
key: Option,
}
@@ -22,27 +34,45 @@ impl BSuspense {
pub fn key(&self) -> Option<&Key> {
self.key.as_ref()
}
- /// Get the bundle node that actually shows up in the dom
- fn active_node(&self) -> &BNode {
- self.fallback_bundle
- .as_ref()
- .unwrap_or(&self.children_bundle)
- }
}
impl ReconcileTarget for BSuspense {
fn detach(self, root: &BSubtree, parent: &Element, parent_to_detach: bool) {
- if let Some(fallback) = self.fallback_bundle {
- fallback.detach(root, parent, parent_to_detach);
- self.children_bundle
- .detach(root, &self.detached_parent, false);
- } else {
- self.children_bundle.detach(root, parent, parent_to_detach);
+ match self.fallback {
+ Some(m) => {
+ match m {
+ Fallback::Bundle(bundle) => {
+ bundle.detach(root, parent, parent_to_detach);
+ }
+
+ #[cfg(feature = "hydration")]
+ Fallback::Fragment(fragment) => {
+ fragment.detach(root, parent, parent_to_detach);
+ }
+ }
+
+ self.children_bundle
+ .detach(root, &self.detached_parent, false);
+ }
+ None => {
+ self.children_bundle.detach(root, parent, parent_to_detach);
+ }
}
}
fn shift(&self, next_parent: &Element, next_sibling: NodeRef) {
- self.active_node().shift(next_parent, next_sibling)
+ match self.fallback.as_ref() {
+ Some(Fallback::Bundle(bundle)) => {
+ bundle.shift(next_parent, next_sibling);
+ }
+ #[cfg(feature = "hydration")]
+ Some(Fallback::Fragment(fragment)) => {
+ fragment.shift(next_parent, next_sibling);
+ }
+ None => {
+ self.children_bundle.shift(next_parent, next_sibling);
+ }
+ }
}
}
@@ -77,7 +107,7 @@ impl Reconcilable for VSuspense {
fallback_ref,
BSuspense {
children_bundle,
- fallback_bundle: Some(fallback),
+ fallback: Some(Fallback::Bundle(fallback)),
detached_parent,
key,
},
@@ -89,7 +119,7 @@ impl Reconcilable for VSuspense {
child_ref,
BSuspense {
children_bundle,
- fallback_bundle: None,
+ fallback: None,
detached_parent,
key,
},
@@ -124,7 +154,7 @@ impl Reconcilable for VSuspense {
) -> NodeRef {
let VSuspense {
children,
- fallback,
+ fallback: vfallback,
suspended,
key: _,
} = self;
@@ -134,9 +164,9 @@ impl Reconcilable 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 (suspended, &mut suspense.fallback_bundle) {
+ match (suspended, &mut suspense.fallback) {
// Both suspended, reconcile children into detached_parent, fallback into the DOM
- (true, Some(fallback_bundle)) => {
+ (true, Some(fallback)) => {
children.reconcile_node(
root,
parent_scope,
@@ -145,7 +175,20 @@ impl Reconcilable for VSuspense {
children_bundle,
);
- fallback.reconcile_node(root, parent_scope, parent, next_sibling, fallback_bundle)
+ match fallback {
+ Fallback::Bundle(bundle) => {
+ vfallback.reconcile_node(root, parent_scope, parent, next_sibling, bundle)
+ }
+ #[cfg(feature = "hydration")]
+ Fallback::Fragment(fragment) => {
+ let node_ref = NodeRef::default();
+ match fragment.front().cloned() {
+ Some(m) => node_ref.set(Some(m)),
+ None => node_ref.link(next_sibling),
+ }
+ node_ref
+ }
+ }
}
// Not suspended, just reconcile the children into the DOM
(false, None) => {
@@ -163,18 +206,26 @@ impl Reconcilable for VSuspense {
children_bundle,
);
// first render of fallback
+
let (fallback_ref, fallback) =
- fallback.attach(root, parent_scope, parent, next_sibling);
- suspense.fallback_bundle = Some(fallback);
+ vfallback.attach(root, parent_scope, parent, next_sibling);
+ suspense.fallback = Some(Fallback::Bundle(fallback));
fallback_ref
}
// Freshly unsuspended. Detach fallback from the DOM, then shift children into it.
(false, Some(_)) => {
- suspense
- .fallback_bundle
- .take()
- .unwrap() // We just matched Some(_)
- .detach(root, parent, false);
+ match suspense.fallback.take() {
+ Some(Fallback::Bundle(bundle)) => {
+ bundle.detach(root, parent, false);
+ }
+ #[cfg(feature = "hydration")]
+ Some(Fallback::Fragment(fragment)) => {
+ fragment.detach(root, parent, false);
+ }
+ None => {
+ unreachable!("None condition has been checked before.")
+ }
+ };
children_bundle.shift(parent, next_sibling.clone());
children.reconcile_node(root, parent_scope, parent, next_sibling, children_bundle)
@@ -182,3 +233,62 @@ impl Reconcilable for VSuspense {
}
}
}
+
+#[cfg(feature = "hydration")]
+mod feat_hydration {
+ use super::*;
+
+ use crate::dom_bundle::{Fragment, Hydratable};
+ use crate::virtual_dom::Collectable;
+
+ impl Hydratable for VSuspense {
+ fn hydrate(
+ self,
+ root: &BSubtree,
+ parent_scope: &AnyScope,
+ parent: &Element,
+ fragment: &mut Fragment,
+ ) -> (NodeRef, Self::Bundle) {
+ let detached_parent = document()
+ .create_element("div")
+ .expect("failed to create detached element");
+
+ let collectable = Collectable::Suspense;
+ let fallback_fragment = Fragment::collect_between(fragment, &collectable, parent);
+
+ let mut nodes = fallback_fragment.deep_clone();
+
+ for node in nodes.iter() {
+ detached_parent.append_child(node).unwrap();
+ }
+
+ let (_, children_bundle) =
+ self.children
+ .hydrate(root, parent_scope, &detached_parent, &mut nodes);
+
+ // 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.");
+
+ let node_ref = fallback_fragment
+ .front()
+ .cloned()
+ .map(NodeRef::new)
+ .unwrap_or_default();
+
+ (
+ node_ref,
+ BSuspense {
+ children_bundle,
+ detached_parent,
+ key: self.key,
+
+ // We start hydration with the BSuspense being suspended.
+ // A subsequent render will resume the BSuspense if not needed to be suspended.
+ fallback: Some(Fallback::Fragment(fallback_fragment)),
+ },
+ )
+ }
+ }
+}
diff --git a/packages/yew/src/dom_bundle/btag/mod.rs b/packages/yew/src/dom_bundle/btag/mod.rs
index c7a63f38849..5f995bb99b2 100644
--- a/packages/yew/src/dom_bundle/btag/mod.rs
+++ b/packages/yew/src/dom_bundle/btag/mod.rs
@@ -287,6 +287,98 @@ impl BTag {
}
}
+#[cfg(feature = "hydration")]
+mod feat_hydration {
+ use super::*;
+
+ use crate::dom_bundle::{node_type_str, Fragment, Hydratable};
+ use web_sys::Node;
+
+ impl Hydratable for VTag {
+ fn hydrate(
+ self,
+ root: &BSubtree,
+ parent_scope: &AnyScope,
+ parent: &Element,
+ fragment: &mut Fragment,
+ ) -> (NodeRef, Self::Bundle) {
+ let tag_name = self.tag().to_owned();
+
+ let Self {
+ inner,
+ listeners,
+ attributes,
+ node_ref,
+ key,
+ } = self;
+
+ // We trim all text nodes as it's likely these are whitespaces.
+ fragment.trim_start_text_nodes(parent);
+
+ let node = fragment
+ .pop_front()
+ .unwrap_or_else(|| panic!("expected element of type {}, found EOF.", tag_name));
+
+ assert_eq!(
+ node.node_type(),
+ Node::ELEMENT_NODE,
+ "expected element, found node type {}.",
+ node_type_str(&node),
+ );
+ let el = node.dyn_into::().expect("expected an element.");
+
+ assert_eq!(
+ el.tag_name().to_lowercase(),
+ tag_name,
+ "expected element of kind {}, found {}.",
+ tag_name,
+ el.tag_name().to_lowercase(),
+ );
+
+ // We simply registers listeners and updates all attributes.
+ let attributes = attributes.apply(root, &el);
+ let listeners = listeners.apply(root, &el);
+
+ // For input and textarea elements, we update their value anyways.
+ let inner = match inner {
+ VTagInner::Input(f) => {
+ let f = f.apply(root, el.unchecked_ref());
+ BTagInner::Input(f)
+ }
+ VTagInner::Textarea { value } => {
+ let value = value.apply(root, el.unchecked_ref());
+
+ BTagInner::Textarea { value }
+ }
+ VTagInner::Other { children, tag } => {
+ let mut nodes = Fragment::collect_children(&el);
+ let (_, child_bundle) = children.hydrate(root, parent_scope, &el, &mut nodes);
+
+ nodes.trim_start_text_nodes(parent);
+
+ assert!(nodes.is_empty(), "expected EOF, found node.");
+
+ BTagInner::Other { child_bundle, tag }
+ }
+ };
+
+ node_ref.set(Some((*el).clone()));
+
+ (
+ node_ref.clone(),
+ BTag {
+ inner,
+ listeners,
+ attributes,
+ reference: el,
+ node_ref,
+ key,
+ },
+ )
+ }
+ }
+}
+
#[cfg(feature = "wasm_test")]
#[cfg(test)]
mod tests {
diff --git a/packages/yew/src/dom_bundle/btext.rs b/packages/yew/src/dom_bundle/btext.rs
index 969ce3eef02..8a0f506fce8 100644
--- a/packages/yew/src/dom_bundle/btext.rs
+++ b/packages/yew/src/dom_bundle/btext.rs
@@ -89,6 +89,66 @@ impl std::fmt::Debug for BText {
}
}
+#[cfg(feature = "hydration")]
+mod feat_hydration {
+ use super::*;
+
+ use web_sys::Node;
+
+ use crate::dom_bundle::{Fragment, Hydratable};
+ use wasm_bindgen::JsCast;
+
+ impl Hydratable for VText {
+ fn hydrate(
+ self,
+ root: &BSubtree,
+ parent_scope: &AnyScope,
+ parent: &Element,
+ fragment: &mut Fragment,
+ ) -> (NodeRef, Self::Bundle) {
+ if let Some(m) = fragment.front().cloned() {
+ // better safe than sorry.
+ if m.node_type() == Node::TEXT_NODE {
+ if let Ok(m) = m.dyn_into::() {
+ // pop current node.
+ fragment.pop_front();
+
+ // TODO: It may make sense to assert the text content in the text node against
+ // the VText when #[cfg(debug_assertions)] is true, but this may be complicated.
+ // We always replace the text value for now.
+ //
+ // Please see the next comment for a detailed explanation.
+ m.set_node_value(Some(self.text.as_ref()));
+
+ return (
+ NodeRef::new(m.clone().into()),
+ BText {
+ text: self.text,
+ text_node: m,
+ },
+ );
+ }
+ }
+ }
+
+ // If there are multiple text nodes placed back-to-back in SSR, it may be parsed as a single
+ // text node by browser, hence we need to add extra text nodes here if the next node is not a text node.
+ // Similarly, the value of the text node may be a combination of multiple VText vnodes.
+ // So we always need to override their values.
+ self.attach(
+ root,
+ parent_scope,
+ parent,
+ fragment
+ .front()
+ .cloned()
+ .map(NodeRef::new)
+ .unwrap_or_default(),
+ )
+ }
+ }
+}
+
#[cfg(test)]
mod test {
extern crate self as yew;
diff --git a/packages/yew/src/dom_bundle/fragment.rs b/packages/yew/src/dom_bundle/fragment.rs
new file mode 100644
index 00000000000..1ce1c1c0773
--- /dev/null
+++ b/packages/yew/src/dom_bundle/fragment.rs
@@ -0,0 +1,166 @@
+use std::collections::VecDeque;
+use std::ops::{Deref, DerefMut};
+
+use web_sys::{Element, Node};
+
+use super::BSubtree;
+use crate::html::NodeRef;
+use crate::virtual_dom::Collectable;
+
+/// A Hydration Fragment
+#[derive(Default, Debug, Clone, PartialEq, Eq)]
+pub(crate) struct Fragment(VecDeque);
+
+impl Deref for Fragment {
+ type Target = VecDeque;
+
+ fn deref(&self) -> &Self::Target {
+ &self.0
+ }
+}
+
+impl DerefMut for Fragment {
+ fn deref_mut(&mut self) -> &mut Self::Target {
+ &mut self.0
+ }
+}
+
+impl Fragment {
+ /// Collects child nodes of an element into a VecDeque.
+ pub fn collect_children(parent: &Element) -> Self {
+ 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.
+ while let Some(m) = current_node {
+ current_node = m.next_sibling();
+ fragment.push_back(m);
+ }
+
+ Self(fragment)
+ }
+
+ /// Collects nodes for a Component Bundle or a BSuspense.
+ pub fn collect_between(
+ collect_from: &mut Fragment,
+ collect_for: &Collectable,
+ parent: &Element,
+ ) -> Self {
+ let is_open_tag = |node: &Node| {
+ let comment_text = node.text_content().unwrap_or_else(|| "".to_string());
+
+ comment_text.starts_with(collect_for.open_start_mark())
+ && comment_text.ends_with(collect_for.end_mark())
+ };
+
+ let is_close_tag = |node: &Node| {
+ let comment_text = node.text_content().unwrap_or_else(|| "".to_string());
+
+ comment_text.starts_with(collect_for.close_start_mark())
+ && comment_text.ends_with(collect_for.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 {} opening tag, found EOF", collect_for.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 {}",
+ collect_for.name(),
+ first_node.node_type()
+ );
+
+ let mut nodes = VecDeque::new();
+
+ if !is_open_tag(&first_node) {
+ panic!(
+ "expected {} opening tag, found comment node",
+ collect_for.name()
+ );
+ }
+
+ // We remove the opening tag.
+ parent.remove_child(&first_node).unwrap();
+
+ let mut nested_layers = 1;
+
+ loop {
+ let current_node = collect_from.pop_front().unwrap_or_else(|| {
+ panic!("expected {} closing tag, found EOF", collect_for.name())
+ });
+
+ if current_node.node_type() == Node::COMMENT_NODE {
+ if is_open_tag(¤t_node) {
+ // We found another opening tag, we need to increase component counter.
+ nested_layers += 1;
+ } 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 end of the current tag we are collecting, breaking
+ // the loop.
+
+ // We remove the closing tag.
+ parent.remove_child(¤t_node).unwrap();
+ break;
+ }
+ }
+ }
+
+ nodes.push_back(current_node.clone());
+ }
+
+ Self(nodes)
+ }
+
+ /// Remove child nodes until first non-text node.
+ pub fn trim_start_text_nodes(&mut self, parent: &Element) {
+ while let Some(ref m) = self.front().cloned() {
+ if m.node_type() == Node::TEXT_NODE {
+ self.pop_front();
+
+ parent.remove_child(m).unwrap();
+ } else {
+ break;
+ }
+ }
+ }
+
+ /// Deeply clones all nodes.
+ pub fn deep_clone(&self) -> Self {
+ let nodes = self
+ .iter()
+ .map(|m| m.clone_node_with_deep(true).expect("failed to clone node."))
+ .collect::>();
+
+ Self(nodes)
+ }
+
+ // detaches current fragment.
+ pub fn detach(self, _root: &BSubtree, parent: &Element, parent_to_detach: bool) {
+ if !parent_to_detach {
+ for node in self.iter() {
+ parent
+ .remove_child(node)
+ .expect("failed to remove child element");
+ }
+ }
+ }
+
+ /// Shift current Fragment into a different position in the dom.
+ pub fn shift(&self, next_parent: &Element, next_sibling: NodeRef) {
+ for node in self.iter() {
+ next_parent
+ .insert_before(node, next_sibling.get().as_ref())
+ .unwrap();
+ }
+ }
+}
diff --git a/packages/yew/src/dom_bundle/mod.rs b/packages/yew/src/dom_bundle/mod.rs
index 16df0b97db3..af5658c825b 100644
--- a/packages/yew/src/dom_bundle/mod.rs
+++ b/packages/yew/src/dom_bundle/mod.rs
@@ -5,6 +5,12 @@
//! In order to efficiently implement updates, and diffing, additional information has to be
//! kept around. This information is carried in the bundle.
+use web_sys::Element;
+
+use crate::html::AnyScope;
+use crate::html::NodeRef;
+use crate::virtual_dom::VNode;
+
mod bcomp;
mod blist;
mod bnode;
@@ -14,31 +20,35 @@ mod btag;
mod btext;
mod subtree_root;
+#[cfg(feature = "hydration")]
+mod fragment;
+
mod traits;
mod utils;
-use web_sys::Element;
-
-use crate::html::AnyScope;
-use crate::html::NodeRef;
-use crate::virtual_dom::VNode;
-
use bcomp::BComp;
use blist::BList;
use bnode::BNode;
use bportal::BPortal;
use bsuspense::BSuspense;
+
use btag::{BTag, Registry};
use btext::BText;
use subtree_root::EventDescriptor;
use traits::{Reconcilable, ReconcileTarget};
use utils::{insert_node, test_log};
-#[doc(hidden)] // Publically exported from crate::events
pub use subtree_root::set_event_bubbling;
pub(crate) use subtree_root::BSubtree;
+#[cfg(feature = "hydration")]
+pub(crate) use fragment::Fragment;
+#[cfg(feature = "hydration")]
+use traits::Hydratable;
+#[cfg(feature = "hydration")]
+use utils::node_type_str;
+
/// A Bundle.
///
/// Each component holds a bundle that represents a realised layout, designated by a [VNode].
@@ -50,6 +60,7 @@ pub(crate) struct Bundle(BNode);
impl Bundle {
/// Creates a new bundle.
+
pub const fn new() -> Self {
Self(BNode::List(BList::new()))
}
@@ -76,3 +87,22 @@ impl Bundle {
self.0.detach(root, parent, parent_to_detach);
}
}
+
+#[cfg(feature = "hydration")]
+mod feat_hydration {
+ use super::*;
+
+ impl Bundle {
+ /// Creates a bundle by hydrating a virtual dom layout.
+ pub fn hydrate(
+ root: &BSubtree,
+ parent_scope: &AnyScope,
+ parent: &Element,
+ fragment: &mut Fragment,
+ node: VNode,
+ ) -> (NodeRef, Self) {
+ let (node_ref, bundle) = node.hydrate(root, parent_scope, parent, fragment);
+ (node_ref, Self(bundle))
+ }
+ }
+}
diff --git a/packages/yew/src/dom_bundle/traits.rs b/packages/yew/src/dom_bundle/traits.rs
index a9c1639e91c..e87f17fc31b 100644
--- a/packages/yew/src/dom_bundle/traits.rs
+++ b/packages/yew/src/dom_bundle/traits.rs
@@ -34,6 +34,7 @@ pub(super) trait Reconcilable {
/// Returns a reference to the newly inserted element.
fn attach(
self,
+
root: &BSubtree,
parent_scope: &AnyScope,
parent: &Element,
@@ -59,6 +60,7 @@ pub(super) trait Reconcilable {
/// Returns a reference to the newly inserted element.
fn reconcile_node(
self,
+
root: &BSubtree,
parent_scope: &AnyScope,
parent: &Element,
@@ -78,6 +80,7 @@ pub(super) trait Reconcilable {
/// Replace an existing bundle by attaching self and detaching the existing one
fn replace(
self,
+
root: &BSubtree,
parent_scope: &AnyScope,
parent: &Element,
@@ -94,3 +97,30 @@ pub(super) trait Reconcilable {
self_ref
}
}
+
+#[cfg(feature = "hydration")]
+mod feat_hydration {
+ use super::*;
+
+ use crate::dom_bundle::Fragment;
+
+ pub(in crate::dom_bundle) trait Hydratable: Reconcilable {
+ /// hydrates current tree.
+ ///
+ /// Returns a reference to the first node of the hydrated tree.
+ ///
+ /// # Important
+ ///
+ /// DOM tree is hydrated from top to bottom. This is different than [`Reconcilable`].
+ fn hydrate(
+ self,
+ root: &BSubtree,
+ parent_scope: &AnyScope,
+ parent: &Element,
+ fragment: &mut Fragment,
+ ) -> (NodeRef, Self::Bundle);
+ }
+}
+
+#[cfg(feature = "hydration")]
+pub(in crate::dom_bundle) use feat_hydration::*;
diff --git a/packages/yew/src/dom_bundle/utils.rs b/packages/yew/src/dom_bundle/utils.rs
index e1385ad5a51..6b93a2b3023 100644
--- a/packages/yew/src/dom_bundle/utils.rs
+++ b/packages/yew/src/dom_bundle/utils.rs
@@ -26,3 +26,41 @@ macro_rules! test_log {
/// Log an operation during tests for debugging purposes
/// Set RUSTFLAGS="--cfg verbose_tests" environment variable to activate.
pub(super) use test_log;
+
+#[cfg(feature = "hydration")]
+mod feat_hydration {
+ use super::*;
+
+ use std::borrow::Cow;
+
+ use wasm_bindgen::JsCast;
+ use web_sys::Element;
+
+ pub(in crate::dom_bundle) fn node_type_str(node: &Node) -> Cow<'static, str> {
+ match node.node_type() {
+ Node::ELEMENT_NODE => {
+ let tag = node
+ .dyn_ref::()
+ .map(|m| m.tag_name().to_lowercase())
+ .unwrap_or_else(|| "unknown".to_owned());
+
+ format!("{} element node", tag).into()
+ }
+ Node::ATTRIBUTE_NODE => "attribute node".into(),
+ Node::TEXT_NODE => "text node".into(),
+ Node::CDATA_SECTION_NODE => "cdata section node".into(),
+ Node::ENTITY_REFERENCE_NODE => "entity reference node".into(),
+ Node::ENTITY_NODE => "entity node".into(),
+ Node::PROCESSING_INSTRUCTION_NODE => "processing instruction node".into(),
+ Node::COMMENT_NODE => "comment node".into(),
+ Node::DOCUMENT_NODE => "document node".into(),
+ Node::DOCUMENT_TYPE_NODE => "document type node".into(),
+ Node::DOCUMENT_FRAGMENT_NODE => "document fragment node".into(),
+ Node::NOTATION_NODE => "notation node".into(),
+ _ => "unknown node".into(),
+ }
+ }
+}
+
+#[cfg(feature = "hydration")]
+pub(super) use feat_hydration::*;
diff --git a/packages/yew/src/functional/mod.rs b/packages/yew/src/functional/mod.rs
index 35226a536d0..520210080bd 100644
--- a/packages/yew/src/functional/mod.rs
+++ b/packages/yew/src/functional/mod.rs
@@ -26,12 +26,13 @@ use std::cell::RefCell;
use std::fmt;
use std::rc::Rc;
+use wasm_bindgen::prelude::*;
+
mod hooks;
pub use hooks::*;
-use crate::html::Context;
-
use crate::html::sealed::SealedBaseComponent;
+use crate::html::Context;
/// This attribute creates a function component from a normal Rust function.
///
@@ -85,6 +86,19 @@ pub struct HookContext {
}
impl HookContext {
+ fn new(scope: AnyScope, re_render: ReRender) -> RefCell {
+ RefCell::new(HookContext {
+ effects: Vec::new(),
+ scope,
+ re_render,
+ states: Vec::new(),
+
+ counter: 0,
+ #[cfg(debug_assertions)]
+ total_hook_counter: None,
+ })
+ }
+
pub(crate) fn next_state(&mut self, initializer: impl FnOnce(ReRender) -> T) -> Rc
where
T: 'static,
@@ -103,7 +117,7 @@ impl HookContext {
}
};
- state.downcast().unwrap()
+ state.downcast().unwrap_throw()
}
pub(crate) fn next_effect(&mut self, initializer: impl FnOnce(ReRender) -> T) -> Rc
@@ -120,6 +134,59 @@ impl HookContext {
t
}
+
+ #[inline(always)]
+ fn prepare_run(&mut self) {
+ self.counter = 0;
+ }
+
+ /// asserts hook counter.
+ ///
+ /// This function asserts that the number of hooks matches for every render.
+ #[cfg(debug_assertions)]
+ fn assert_hook_context(&mut self, render_ok: bool) {
+ // Procedural Macros can catch most conditionally called hooks at compile time, but it cannot
+ // detect early return (as the return can be Err(_), Suspension).
+ match (render_ok, self.total_hook_counter) {
+ // First rendered,
+ // we store the hook counter.
+ (true, None) => {
+ self.total_hook_counter = Some(self.counter);
+ }
+ // Component is suspended before it's first rendered.
+ // We don't have a total count to compare with.
+ (false, None) => {}
+
+ // Subsequent render,
+ // we compare stored total count and current render count.
+ (true, Some(total_hook_counter)) => assert_eq!(
+ total_hook_counter, self.counter,
+ "Hooks are called conditionally."
+ ),
+
+ // Subsequent suspension,
+ // components can have less hooks called when suspended, but not more.
+ (false, Some(total_hook_counter)) => assert!(
+ self.counter <= total_hook_counter,
+ "Hooks are called conditionally."
+ ),
+ }
+ }
+
+ fn run_effects(&self) {
+ for effect in self.effects.iter() {
+ effect.rendered();
+ }
+ }
+
+ fn drain_states(&mut self) {
+ // We clear the effects as these are also references to states.
+ self.effects.clear();
+
+ for state in self.states.drain(..) {
+ drop(state);
+ }
+ }
}
impl fmt::Debug for HookContext {
@@ -167,21 +234,14 @@ where
fn create(ctx: &Context) -> Self {
let scope = AnyScope::from(ctx.link().clone());
+ let re_render = {
+ let link = ctx.link().clone();
+ Rc::new(move || link.send_message(()))
+ };
+
Self {
_never: std::marker::PhantomData::default(),
- hook_ctx: RefCell::new(HookContext {
- effects: Vec::new(),
- scope,
- re_render: {
- let link = ctx.link().clone();
- Rc::new(move || link.send_message(()))
- },
- states: Vec::new(),
-
- counter: 0,
- #[cfg(debug_assertions)]
- total_hook_counter: None,
- }),
+ hook_ctx: HookContext::new(scope, re_render),
}
}
@@ -195,56 +255,27 @@ where
fn view(&self, ctx: &Context) -> HtmlResult {
let props = ctx.props();
- let mut ctx = self.hook_ctx.borrow_mut();
- ctx.counter = 0;
+ let mut hook_ctx = self.hook_ctx.borrow_mut();
+
+ hook_ctx.prepare_run();
#[allow(clippy::let_and_return)]
- let result = T::run(&mut *ctx, props);
+ let result = T::run(&mut *hook_ctx, props);
#[cfg(debug_assertions)]
- {
- // Procedural Macros can catch most conditionally called hooks at compile time, but it cannot
- // detect early return (as the return can be Err(_), Suspension).
- if result.is_err() {
- if let Some(m) = ctx.total_hook_counter {
- // Suspended Components can have less hooks called when suspended, but not more.
- if m < ctx.counter {
- panic!("Hooks are called conditionally.");
- }
- }
- } else {
- match ctx.total_hook_counter {
- Some(m) => {
- if m != ctx.counter {
- panic!("Hooks are called conditionally.");
- }
- }
- None => {
- ctx.total_hook_counter = Some(ctx.counter);
- }
- }
- }
- }
+ hook_ctx.assert_hook_context(result.is_ok());
result
}
fn rendered(&mut self, _ctx: &Context, _first_render: bool) {
let hook_ctx = self.hook_ctx.borrow();
-
- for effect in hook_ctx.effects.iter() {
- effect.rendered();
- }
+ hook_ctx.run_effects();
}
fn destroy(&mut self, _ctx: &Context) {
let mut hook_ctx = self.hook_ctx.borrow_mut();
- // We clear the effects as these are also references to states.
- hook_ctx.effects.clear();
-
- for state in hook_ctx.states.drain(..) {
- drop(state);
- }
+ hook_ctx.drain_states();
}
}
diff --git a/packages/yew/src/html/component/children.rs b/packages/yew/src/html/component/children.rs
index c0fbd1d858d..69368c0cdc3 100644
--- a/packages/yew/src/html/component/children.rs
+++ b/packages/yew/src/html/component/children.rs
@@ -2,6 +2,7 @@
use crate::html::Html;
use crate::virtual_dom::{VChild, VNode};
+use crate::Properties;
use std::fmt;
/// A type used for accepting children elements in Component::Properties.
@@ -208,3 +209,11 @@ impl IntoIterator for ChildrenRenderer {
self.children.into_iter()
}
}
+
+/// A [Properties] type with Children being the only property.
+#[derive(Debug, Properties, PartialEq)]
+pub struct ChildrenProps {
+ /// The Children of a Component.
+ #[prop_or_default]
+ pub children: Children,
+}
diff --git a/packages/yew/src/html/component/lifecycle.rs b/packages/yew/src/html/component/lifecycle.rs
index 5d5e43a6dc6..91ed62f2c0b 100644
--- a/packages/yew/src/html/component/lifecycle.rs
+++ b/packages/yew/src/html/component/lifecycle.rs
@@ -1,7 +1,10 @@
//! Component lifecycle module
use super::scope::{AnyScope, Scope};
+
use super::BaseComponent;
+#[cfg(feature = "hydration")]
+use crate::html::RenderMode;
use crate::html::{Html, RenderError};
use crate::scheduler::{self, Runnable, Shared};
use crate::suspense::{BaseSuspense, Suspension};
@@ -9,6 +12,8 @@ use crate::{Callback, Context, HtmlResult};
use std::any::Any;
use std::rc::Rc;
+#[cfg(feature = "hydration")]
+use crate::dom_bundle::Fragment;
#[cfg(feature = "csr")]
use crate::dom_bundle::{BSubtree, Bundle};
#[cfg(feature = "csr")]
@@ -25,6 +30,14 @@ pub(crate) enum ComponentRenderState {
next_sibling: NodeRef,
node_ref: NodeRef,
},
+ #[cfg(feature = "hydration")]
+ Hydration {
+ parent: Element,
+ next_sibling: NodeRef,
+ node_ref: NodeRef,
+ root: BSubtree,
+ fragment: Fragment,
+ },
#[cfg(feature = "ssr")]
Ssr {
@@ -38,7 +51,7 @@ impl std::fmt::Debug for ComponentRenderState {
#[cfg(feature = "csr")]
Self::Render {
ref bundle,
- ref root,
+ root,
ref parent,
ref next_sibling,
ref node_ref,
@@ -51,6 +64,22 @@ impl std::fmt::Debug for ComponentRenderState {
.field("node_ref", node_ref)
.finish(),
+ #[cfg(feature = "hydration")]
+ Self::Hydration {
+ ref fragment,
+ ref parent,
+ ref next_sibling,
+ ref node_ref,
+ ref root,
+ } => f
+ .debug_struct("ComponentRenderState::Hydration")
+ .field("fragment", fragment)
+ .field("root", root)
+ .field("parent", parent)
+ .field("next_sibling", next_sibling)
+ .field("node_ref", node_ref)
+ .finish(),
+
#[cfg(feature = "ssr")]
Self::Ssr { ref sender } => {
let sender_repr = match sender {
@@ -82,6 +111,18 @@ impl ComponentRenderState {
*parent = next_parent;
*next_sibling = next_next_sibling;
}
+ #[cfg(feature = "hydration")]
+ Self::Hydration {
+ fragment,
+ parent,
+ next_sibling,
+ ..
+ } => {
+ fragment.shift(&next_parent, next_next_sibling.clone());
+
+ *parent = next_parent;
+ *next_sibling = next_next_sibling;
+ }
#[cfg(feature = "ssr")]
Self::Ssr { .. } => {
@@ -192,7 +233,22 @@ impl ComponentState {
props: Rc,
) -> Self {
let comp_id = scope.id;
- let context = Context { scope, props };
+ #[cfg(feature = "hydration")]
+ let mode = {
+ match initial_render_state {
+ ComponentRenderState::Render { .. } => RenderMode::Render,
+ ComponentRenderState::Hydration { .. } => RenderMode::Hydration,
+ #[cfg(feature = "ssr")]
+ ComponentRenderState::Ssr { .. } => RenderMode::Ssr,
+ }
+ };
+
+ let context = Context {
+ scope,
+ props,
+ #[cfg(feature = "hydration")]
+ mode,
+ };
let inner = Box::new(CompStateInner {
component: COMP::create(&context),
@@ -280,6 +336,20 @@ impl Runnable for UpdateRunner {
state.inner.props_changed(props)
}
+ #[cfg(feature = "hydration")]
+ ComponentRenderState::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")]
ComponentRenderState::Ssr { .. } => {
#[cfg(debug_assertions)]
@@ -331,14 +401,27 @@ impl Runnable for DestroyRunner {
ComponentRenderState::Render {
bundle,
ref parent,
- ref root,
ref node_ref,
+ ref root,
..
} => {
bundle.detach(root, 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")]
+ ComponentRenderState::Hydration {
+ ref root,
+ fragment,
+ ref parent,
+ ref node_ref,
+ ..
+ } => {
+ fragment.detach(root, parent, self.parent_to_detach);
+
+ node_ref.set(None);
+ }
#[cfg(feature = "ssr")]
ComponentRenderState::Ssr { .. } => {}
@@ -436,6 +519,7 @@ impl RenderRunner {
..
} => {
let scope = state.inner.any_scope();
+
let new_node_ref =
bundle.reconcile(root, &scope, parent, next_sibling.clone(), new_root);
node_ref.link(new_node_ref);
@@ -453,6 +537,46 @@ impl RenderRunner {
);
}
+ #[cfg(feature = "hydration")]
+ ComponentRenderState::Hydration {
+ ref mut fragment,
+ ref parent,
+ ref node_ref,
+ ref next_sibling,
+ ref root,
+ } => {
+ // We schedule a "first" render to run immediately after hydration,
+ // to fix NodeRefs (first_node and next_sibling).
+ scheduler::push_component_first_render(
+ state.comp_id,
+ Box::new(RenderRunner {
+ state: self.state.clone(),
+ }),
+ );
+
+ let scope = state.inner.any_scope();
+
+ // 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, bundle) = Bundle::hydrate(root, &scope, parent, fragment, new_root);
+
+ // 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");
+
+ node_ref.link(node);
+
+ state.render_state = ComponentRenderState::Render {
+ root: root.clone(),
+ bundle,
+ parent: parent.clone(),
+ node_ref: node_ref.clone(),
+ next_sibling: next_sibling.clone(),
+ };
+ }
+
#[cfg(feature = "ssr")]
ComponentRenderState::Ssr { ref mut sender } => {
if let Some(tx) = sender.take() {
diff --git a/packages/yew/src/html/component/marker.rs b/packages/yew/src/html/component/marker.rs
new file mode 100644
index 00000000000..42c61216063
--- /dev/null
+++ b/packages/yew/src/html/component/marker.rs
@@ -0,0 +1,147 @@
+//! Primitive Components & Properties Types
+
+use crate::function_component;
+use crate::html;
+use crate::html::{ChildrenProps, Html, IntoComponent};
+
+/// 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
+/// 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.
+///
+/// # Warning
+///
+/// The Real DOM hierarchy must also match the server-side rendered artifact. This component is
+/// only usable when the original component does not introduce any additional elements. (e.g.: Context
+/// Providers)
+///
+/// A generic parameter is provided to help identify the component to be substituted.
+/// The type of the generic parameter is not required to be the same component that was in the other
+/// implementation. However, this behaviour may change in the future if more debug assertions were
+/// to be introduced. It is recommended that the generic parameter represents the component in the
+/// other implementation.
+///
+/// # Example
+///
+/// ```
+/// use yew::prelude::*;
+/// # use yew::html::ChildrenProps;
+/// #
+/// # #[function_component]
+/// # fn Comp(props: &ChildrenProps) -> Html {
+/// # Html::default()
+/// # }
+/// #
+/// # #[function_component]
+/// # fn Provider(props: &ChildrenProps) -> Html {
+/// # let children = props.children.clone();
+/// #
+/// # html! { <>{children}> }
+/// # }
+/// # type Provider1 = Provider;
+/// # type Provider2 = Provider;
+/// # type Provider3 = Provider;
+/// # type Provider4 = Provider;
+///
+/// #[function_component]
+/// fn ServerApp() -> Html {
+/// // The Server Side Rendering Application has 3 Providers.
+/// html! {
+///
+///
+///
+///
+///
+///
+///
+/// }
+/// }
+///
+/// #[function_component]
+/// fn App() -> Html {
+/// // The Client Side Rendering Application has 4 Providers.
+/// html! {
+///
+///
+///
+///
+/// // This provider does not exist on the server-side
+/// // Hydration will fail due to Virtual DOM layout mismatch.
+///
+///
+///
+///
+///
+///
+///
+/// }
+/// }
+/// ```
+///
+/// To mitigate this, we can use a `PhantomComponent`:
+///
+/// ```
+/// use yew::prelude::*;
+/// # use yew::html::{PhantomComponent, ChildrenProps};
+/// #
+/// # #[function_component]
+/// # fn Comp(props: &ChildrenProps) -> Html {
+/// # Html::default()
+/// # }
+/// #
+/// # #[function_component]
+/// # fn Provider(props: &ChildrenProps) -> Html {
+/// # let children = props.children.clone();
+/// #
+/// # html! { <>{children}> }
+/// # }
+/// # type Provider1 = Provider;
+/// # type Provider2 = Provider;
+/// # type Provider3 = Provider;
+/// # type Provider4 = Provider;
+///
+/// #[function_component]
+/// fn ServerApp() -> Html {
+/// html! {
+///
+///
+///
+/// // We add a PhantomComponent for Provider4,
+/// // it acts if a Provider4 component presents in this position.
+/// >
+///
+/// >
+///
+///
+///
+/// }
+/// }
+///
+/// #[function_component]
+/// fn App() -> Html {
+/// html! {
+///
+///
+///
+///
+/// // Hydration will succeed as the PhantomComponent in the server-side
+/// // implementation will represent a Provider4 component in this position.
+///
+///
+///
+///
+///
+///
+///
+/// }
+/// }
+/// ```
+#[function_component]
+pub fn PhantomComponent(props: &ChildrenProps) -> Html
+where
+ T: IntoComponent,
+{
+ html! { <>{props.children.clone()}> }
+}
diff --git a/packages/yew/src/html/component/mod.rs b/packages/yew/src/html/component/mod.rs
index 613a664b48a..c196d5e9aaa 100644
--- a/packages/yew/src/html/component/mod.rs
+++ b/packages/yew/src/html/component/mod.rs
@@ -3,12 +3,15 @@
mod children;
#[cfg(any(feature = "csr", feature = "ssr"))]
mod lifecycle;
+mod marker;
mod properties;
mod scope;
use super::{Html, HtmlResult, IntoHtmlResult};
pub use children::*;
+pub use marker::*;
pub use properties::*;
+
#[cfg(feature = "csr")]
pub(crate) use scope::Scoped;
pub use scope::{AnyScope, Scope, SendAsMessage};
@@ -44,6 +47,15 @@ mod feat_csr_ssr {
}
}
+#[cfg(feature = "hydration")]
+#[derive(Debug, Clone, Copy, PartialEq)]
+pub(crate) enum RenderMode {
+ Hydration,
+ Render,
+ #[cfg(feature = "ssr")]
+ Ssr,
+}
+
#[cfg(debug_assertions)]
#[cfg(any(feature = "csr", feature = "ssr"))]
pub(crate) use feat_csr_ssr::*;
@@ -54,6 +66,8 @@ pub(crate) use feat_csr_ssr::*;
pub struct Context {
scope: Scope,
props: Rc,
+ #[cfg(feature = "hydration")]
+ mode: RenderMode,
}
impl Context {
@@ -68,6 +82,11 @@ impl Context {
pub fn props(&self) -> &COMP::Properties {
&*self.props
}
+
+ #[cfg(feature = "hydration")]
+ pub(crate) fn mode(&self) -> RenderMode {
+ self.mode
+ }
}
pub(crate) mod sealed {
diff --git a/packages/yew/src/html/component/scope.rs b/packages/yew/src/html/component/scope.rs
index 7cae98cecc9..dd023bb9854 100644
--- a/packages/yew/src/html/component/scope.rs
+++ b/packages/yew/src/html/component/scope.rs
@@ -43,16 +43,6 @@ impl From> for AnyScope {
}
impl AnyScope {
- #[cfg(feature = "csr")]
- #[cfg(test)]
- pub(crate) fn test() -> Self {
- Self {
- type_id: TypeId::of::<()>(),
- parent: None,
- typed_scope: Rc::new(()),
- }
- }
-
/// Returns the parent scope
pub fn get_parent(&self) -> Option<&AnyScope> {
self.parent.as_deref()
@@ -417,6 +407,17 @@ mod feat_csr {
use std::cell::Ref;
use web_sys::Element;
+ impl AnyScope {
+ #[cfg(test)]
+ pub(crate) fn test() -> Self {
+ Self {
+ type_id: TypeId::of::<()>(),
+ parent: None,
+ typed_scope: Rc::new(()),
+ }
+ }
+ }
+
impl Scope
where
COMP: BaseComponent,
@@ -424,6 +425,7 @@ mod feat_csr {
/// Mounts a component with `props` to the specified `element` in the DOM.
pub(crate) fn mount_in_place(
&self,
+
root: BSubtree,
parent: Element,
next_sibling: NodeRef,
@@ -518,6 +520,83 @@ mod feat_csr {
}
}
+#[cfg_attr(documenting, doc(cfg(feature = "hydration")))]
+#[cfg(feature = "hydration")]
+mod feat_hydration {
+ use super::*;
+
+ use crate::dom_bundle::{BSubtree, Fragment};
+ use crate::html::component::lifecycle::{ComponentRenderState, CreateRunner, RenderRunner};
+ use crate::html::NodeRef;
+ use crate::scheduler;
+ use crate::virtual_dom::Collectable;
+
+ use web_sys::Element;
+
+ impl Scope
+ where
+ COMP: BaseComponent,
+ {
+ /// Hydrates the component.
+ ///
+ /// Returns a pending 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,
+ root: BSubtree,
+ parent: Element,
+ fragment: &mut Fragment,
+ node_ref: NodeRef,
+ props: Rc,
+ ) {
+ // 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(all(debug_assertions, feature = "trace_hydration"))]
+ gloo::console::trace!(format!(
+ "queuing hydration of: {}(ID: {:?})",
+ std::any::type_name::(),
+ self.id
+ ));
+
+ #[cfg(debug_assertions)]
+ let collectable = Collectable::Component(std::any::type_name::());
+ #[cfg(not(debug_assertions))]
+ let collectable = Collectable::Component;
+
+ let fragment = Fragment::collect_between(fragment, &collectable, &parent);
+ node_ref.set(fragment.front().cloned());
+ let next_sibling = NodeRef::default();
+
+ let state = ComponentRenderState::Hydration {
+ root,
+ parent,
+ node_ref,
+ next_sibling,
+ fragment,
+ };
+
+ scheduler::push_component_create(
+ self.id,
+ Box::new(CreateRunner {
+ initial_render_state: state,
+ props,
+ scope: self.clone(),
+ }),
+ Box::new(RenderRunner {
+ state: self.state.clone(),
+ }),
+ );
+
+ // Not guaranteed to already have the scheduler started
+ scheduler::start();
+ }
+ }
+}
#[cfg(feature = "csr")]
pub(crate) use feat_csr::*;
diff --git a/packages/yew/src/lib.rs b/packages/yew/src/lib.rs
index 7227be1c9db..fe96c0a83df 100644
--- a/packages/yew/src/lib.rs
+++ b/packages/yew/src/lib.rs
@@ -24,6 +24,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
//!
@@ -280,6 +283,7 @@ pub mod utils;
pub mod virtual_dom;
#[cfg(feature = "ssr")]
pub use server_renderer::*;
+
#[cfg(feature = "csr")]
mod app_handle;
#[cfg(feature = "csr")]
@@ -317,6 +321,7 @@ pub mod prelude {
//! # #![allow(unused_imports)]
//! use yew::prelude::*;
//! ```
+
#[cfg(feature = "csr")]
pub use crate::app_handle::AppHandle;
pub use crate::callback::Callback;
diff --git a/packages/yew/src/renderer.rs b/packages/yew/src/renderer.rs
index c51a3e37a58..324349f7f92 100644
--- a/packages/yew/src/renderer.rs
+++ b/packages/yew/src/renderer.rs
@@ -92,3 +92,20 @@ where
AppHandle::::mount_with_props(self.root, Rc::new(self.props))
}
}
+
+#[cfg_attr(documenting, doc(cfg(feature = "hydration")))]
+#[cfg(feature = "hydration")]
+mod feat_hydration {
+ use super::*;
+
+ impl Renderer
+ where
+ ICOMP: IntoComponent + 'static,
+ {
+ /// Hydrates the application.
+ pub fn hydrate(self) -> AppHandle {
+ set_default_panic_hook();
+ AppHandle::::hydrate_with_props(self.root, Rc::new(self.props))
+ }
+ }
+}
diff --git a/packages/yew/src/scheduler.rs b/packages/yew/src/scheduler.rs
index 26b914d4909..9abc7f347de 100644
--- a/packages/yew/src/scheduler.rs
+++ b/packages/yew/src/scheduler.rs
@@ -80,7 +80,7 @@ mod feat_csr_ssr {
with(|s| s.destroy.push(runnable));
}
- /// Push a component render and rendered [Runnable]s to be executed
+ /// Push a component render [Runnable]s to be executed
pub(crate) fn push_component_render(component_id: usize, render: Box) {
with(|s| {
s.render.insert(component_id, render);
@@ -115,6 +115,20 @@ mod feat_csr {
}
}
+#[cfg(feature = "hydration")]
+mod feat_hydration {
+ use super::*;
+
+ pub(crate) fn push_component_first_render(component_id: usize, render: Box) {
+ with(|s| {
+ s.render_first.insert(component_id, render);
+ });
+ }
+}
+
+#[cfg(feature = "hydration")]
+pub(crate) use feat_hydration::*;
+
#[cfg(feature = "csr")]
pub(crate) use feat_csr::*;
diff --git a/packages/yew/src/suspense/component.rs b/packages/yew/src/suspense/component.rs
index 4bcdbe43664..2275bd73b4c 100644
--- a/packages/yew/src/suspense/component.rs
+++ b/packages/yew/src/suspense/component.rs
@@ -13,15 +13,20 @@ pub struct SuspenseProps {
mod feat_csr_ssr {
use super::*;
+ #[cfg(feature = "hydration")]
+ use crate::callback::Callback;
+ #[cfg(feature = "hydration")]
+ use crate::html::RenderMode;
use crate::html::{Children, Component, Context, Html, Scope};
use crate::suspense::Suspension;
+ #[cfg(feature = "hydration")]
+ use crate::suspense::SuspensionHandle;
use crate::virtual_dom::{VNode, VSuspense};
use crate::{function_component, html};
#[derive(Properties, PartialEq, Debug, Clone)]
pub(crate) struct BaseSuspenseProps {
pub children: Children,
-
pub fallback: Option,
}
@@ -35,6 +40,8 @@ mod feat_csr_ssr {
pub(crate) struct BaseSuspense {
link: Scope,
suspensions: Vec,
+ #[cfg(feature = "hydration")]
+ hydration_handle: Option,
}
impl Component for BaseSuspense {
@@ -42,9 +49,30 @@ mod feat_csr_ssr {
type Message = BaseSuspenseMsg;
fn create(ctx: &Context) -> Self {
+ #[cfg(not(feature = "hydration"))]
+ let suspensions = Vec::new();
+
+ // We create a suspension to block suspense until its rendered method is notified.
+ #[cfg(feature = "hydration")]
+ let (suspensions, hydration_handle) = {
+ match ctx.mode() {
+ RenderMode::Hydration => {
+ let link = ctx.link().clone();
+ let (s, handle) = Suspension::new();
+ s.listen(Callback::from(move |s| {
+ link.send_message(BaseSuspenseMsg::Resume(s));
+ }));
+ (vec![s], Some(handle))
+ }
+ _ => (Vec::new(), None),
+ }
+ };
+
Self {
link: ctx.link().clone(),
- suspensions: Vec::new(),
+ suspensions,
+ #[cfg(feature = "hydration")]
+ hydration_handle,
}
}
@@ -94,6 +122,15 @@ mod feat_csr_ssr {
None => children,
}
}
+
+ #[cfg(feature = "hydration")]
+ fn rendered(&mut self, _ctx: &Context, first_render: bool) {
+ if first_render {
+ if let Some(m) = self.hydration_handle.take() {
+ m.resume();
+ }
+ }
+ }
}
impl BaseSuspense {
diff --git a/packages/yew/src/virtual_dom/mod.rs b/packages/yew/src/virtual_dom/mod.rs
index 2970eae9874..b1e43204cba 100644
--- a/packages/yew/src/virtual_dom/mod.rs
+++ b/packages/yew/src/virtual_dom/mod.rs
@@ -179,7 +179,7 @@ mod tests_attr_value {
}
}
-#[cfg(feature = "ssr")] // & feature = "hydration"
+#[cfg(any(feature = "ssr", feature = "hydration"))]
mod feat_ssr_hydration {
/// A collectable.
///
@@ -251,10 +251,21 @@ mod feat_ssr_hydration {
w.push_str(self.end_mark());
w.push_str("-->");
}
+
+ #[cfg(feature = "hydration")]
+ pub fn name(&self) -> super::Cow<'static, str> {
+ match self {
+ #[cfg(debug_assertions)]
+ Self::Component(m) => format!("Component({})", m).into(),
+ #[cfg(not(debug_assertions))]
+ Self::Component => "Component".into(),
+ Self::Suspense => "Suspense".into(),
+ }
+ }
}
}
-#[cfg(feature = "ssr")]
+#[cfg(any(feature = "ssr", feature = "hydration"))]
pub(crate) use feat_ssr_hydration::*;
/// A collection of attributes for an element
diff --git a/packages/yew/src/virtual_dom/vcomp.rs b/packages/yew/src/virtual_dom/vcomp.rs
index e380ac5065a..253d7f1c5c4 100644
--- a/packages/yew/src/virtual_dom/vcomp.rs
+++ b/packages/yew/src/virtual_dom/vcomp.rs
@@ -11,6 +11,8 @@ use crate::html::{AnyScope, Scope};
#[cfg(feature = "csr")]
use crate::dom_bundle::BSubtree;
+#[cfg(feature = "hydration")]
+use crate::dom_bundle::Fragment;
#[cfg(feature = "csr")]
use crate::html::Scoped;
#[cfg(feature = "csr")]
@@ -72,6 +74,16 @@ pub(crate) trait Mountable {
parent_scope: &'a AnyScope,
hydratable: bool,
) -> LocalBoxFuture<'a, ()>;
+
+ #[cfg(feature = "hydration")]
+ fn hydrate(
+ self: Box,
+ root: BSubtree,
+ parent_scope: &AnyScope,
+ parent: Element,
+ fragment: &mut Fragment,
+ node_ref: NodeRef,
+ ) -> Box;
}
pub(crate) struct PropsWrapper {
@@ -128,6 +140,21 @@ impl Mountable for PropsWrapper {
}
.boxed_local()
}
+
+ #[cfg(feature = "hydration")]
+ fn hydrate(
+ self: Box,
+ root: BSubtree,
+ parent_scope: &AnyScope,
+ parent: Element,
+ fragment: &mut Fragment,
+ node_ref: NodeRef,
+ ) -> Box {
+ let scope: Scope = Scope::new(Some(parent_scope.clone()));
+ scope.hydrate_in_place(root, parent, fragment, node_ref, self.props);
+
+ Box::new(scope)
+ }
}
/// A virtual child component.
@@ -258,9 +285,10 @@ mod ssr_tests {
}
}
- let renderer = ServerRenderer::::new();
-
- let s = renderer.render().await;
+ let s = ServerRenderer::::new()
+ .hydratable(false)
+ .render()
+ .await;
assert_eq!(
s,
diff --git a/packages/yew/tests/failed_tests/base_component_impl-fail.stderr b/packages/yew/tests/failed_tests/base_component_impl-fail.stderr
index 563ac8e341e..a7e774e1dc7 100644
--- a/packages/yew/tests/failed_tests/base_component_impl-fail.stderr
+++ b/packages/yew/tests/failed_tests/base_component_impl-fail.stderr
@@ -1,12 +1,12 @@
error[E0277]: the trait bound `Comp: yew::Component` is not satisfied
- --> tests/failed_tests/base_component_impl-fail.rs:6:6
- |
-6 | impl BaseComponent for Comp {
- | ^^^^^^^^^^^^^ the trait `yew::Component` is not implemented for `Comp`
- |
- = note: required because of the requirements on the impl of `html::component::sealed::SealedBaseComponent` for `Comp`
+ --> tests/failed_tests/base_component_impl-fail.rs:6:6
+ |
+6 | impl BaseComponent for Comp {
+ | ^^^^^^^^^^^^^ the trait `yew::Component` is not implemented for `Comp`
+ |
+ = note: required because of the requirements on the impl of `html::component::sealed::SealedBaseComponent` for `Comp`
note: required by a bound in `BaseComponent`
- --> src/html/component/mod.rs
- |
- | pub trait BaseComponent: sealed::SealedBaseComponent + Sized + 'static {
- | ^^^^^^^^^^^^^^^^^^^^^^^^^^^ required by this bound in `BaseComponent`
+ --> src/html/component/mod.rs
+ |
+ | pub trait BaseComponent: sealed::SealedBaseComponent + Sized + 'static {
+ | ^^^^^^^^^^^^^^^^^^^^^^^^^^^ required by this bound in `BaseComponent`
diff --git a/packages/yew/tests/hydration.rs b/packages/yew/tests/hydration.rs
new file mode 100644
index 00000000000..6becd1c9678
--- /dev/null
+++ b/packages/yew/tests/hydration.rs
@@ -0,0 +1,542 @@
+#![cfg(feature = "hydration")]
+
+use std::rc::Rc;
+use std::time::Duration;
+
+mod common;
+
+use common::{obtain_result, obtain_result_by_id};
+
+use gloo::timers::future::sleep;
+use wasm_bindgen::JsCast;
+use wasm_bindgen_futures::spawn_local;
+use wasm_bindgen_test::*;
+use web_sys::{HtmlElement, HtmlTextAreaElement};
+use yew::prelude::*;
+use yew::suspense::{Suspension, SuspensionResult};
+use yew::{Renderer, ServerRenderer};
+
+wasm_bindgen_test::wasm_bindgen_test_configure!(run_in_browser);
+
+#[wasm_bindgen_test]
+async fn hydration_works() {
+ #[function_component]
+ fn Comp() -> Html {
+ let ctr = use_state_eq(|| 0);
+
+ let onclick = {
+ let ctr = ctr.clone();
+
+ Callback::from(move |_| {
+ ctr.set(*ctr + 1);
+ })
+ };
+
+ html! {
+
+ {"Counter: "}{*ctr}
+ {"+1"}
+
+ }
+ }
+
+ #[function_component]
+ fn App() -> Html {
+ 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::ZERO).await;
+
+ let result = obtain_result_by_id("output");
+
+ // no placeholders, hydration is successful.
+ assert_eq!(
+ result,
+ r#""#
+ );
+
+ gloo_utils::document()
+ .query_selector(".increase")
+ .unwrap()
+ .unwrap()
+ .dyn_into::()
+ .unwrap()
+ .click();
+
+ sleep(Duration::ZERO).await;
+
+ let result = obtain_result_by_id("output");
+
+ assert_eq!(
+ result,
+ r#""#
+ );
+}
+
+#[wasm_bindgen_test]
+async fn hydration_with_suspense() {
+ #[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(Content)]
+ fn content() -> HtmlResult {
+ let resleep = use_sleep()?;
+
+ let value = use_state(|| 0);
+
+ let on_increment = {
+ let value = value.clone();
+
+ Callback::from(move |_: MouseEvent| {
+ value.set(*value + 1);
+ })
+ };
+
+ let on_take_a_break = Callback::from(move |_: MouseEvent| (resleep.clone())());
+
+ Ok(html! {
+
+
{*value}
+
{"increase"}
+
+ {"Take a break!"}
+
+
+ })
+ }
+
+ #[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();
+
+ // still hydrating, during hydration, the server rendered result is shown.
+ assert_eq!(
+ result.as_str(),
+ r#""#
+ );
+
+ sleep(Duration::from_millis(50)).await;
+
+ let result = obtain_result();
+
+ // hydrated.
+ assert_eq!(
+ result.as_str(),
+ r#""#
+ );
+
+ gloo_utils::document()
+ .query_selector(".increase")
+ .unwrap()
+ .unwrap()
+ .dyn_into::()
+ .unwrap()
+ .click();
+
+ sleep(Duration::from_millis(50)).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_with_suspense_not_suspended_at_start() {
+ #[derive(PartialEq)]
+ pub struct SleepState {
+ s: Option,
+ }
+
+ impl SleepState {
+ fn new() -> Self {
+ Self { s: None }
+ }
+ }
+
+ impl Reducible for SleepState {
+ type Action = ();
+
+ fn reduce(self: Rc, _action: Self::Action) -> Rc {
+ let (s, handle) = Suspension::new();
+
+ spawn_local(async move {
+ sleep(Duration::from_millis(50)).await;
+
+ handle.resume();
+ });
+
+ Self { s: Some(s) }.into()
+ }
+ }
+
+ #[hook]
+ pub fn use_sleep() -> SuspensionResult> {
+ let sleep_state = use_reducer(SleepState::new);
+
+ let s = match sleep_state.s.clone() {
+ Some(m) => m,
+ None => return Ok(Rc::new(move || sleep_state.dispatch(()))),
+ };
+
+ if s.resumed() {
+ Ok(Rc::new(move || sleep_state.dispatch(())))
+ } else {
+ Err(s)
+ }
+ }
+
+ #[function_component(Content)]
+ fn content() -> HtmlResult {
+ let resleep = use_sleep()?;
+
+ let value = use_state(|| "I am writing a long story...".to_string());
+
+ let on_text_input = {
+ let value = value.clone();
+
+ Callback::from(move |e: InputEvent| {
+ let input: HtmlTextAreaElement = e.target_unchecked_into();
+
+ value.set(input.value());
+ })
+ };
+
+ let on_take_a_break = Callback::from(move |_| (resleep.clone())());
+
+ Ok(html! {
+
+
+
+ {"Take a break!"}
+
+
+ })
+ }
+
+ #[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! {
+
+
+ {"Take a break!"}
+
+
+ })
+ }
+
+ #[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! {
+
+
+ {"Take a break!"}
+
+
+
+
+
+ })
+ }
+
+ #[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#"Take a break!
wait...(inner)
"#
+ );
+
+ sleep(Duration::from_millis(50)).await;
+
+ let result = obtain_result();
+ assert_eq!(
+ result.as_str(),
+ r#""#
+ );
+}
diff --git a/website/docs/advanced-topics/server-side-rendering.md b/website/docs/advanced-topics/server-side-rendering.md
index d69dda3cbdc..fda3bd24a46 100644
--- a/website/docs/advanced-topics/server-side-rendering.md
+++ b/website/docs/advanced-topics/server-side-rendering.md
@@ -126,11 +126,67 @@ suspended.
With this approach, developers can build a client-agnostic, SSR ready
application with data fetching with very little effort.
+## SSR Hydration
+
+Hydration is the process that connects a Yew application to the
+server-side generated HTML file. By default, `ServerRender` prints
+hydratable html string which includes additional information to facilitate hydration.
+When the `Renderer::hydrate` method is called, instead of start rendering from
+scratch, Yew will reconcile the Virtual DOM generated by the application
+with the html string generated by the server renderer.
+
+:::caution
+
+To successfully hydrate an html representation created by the
+`ServerRenderer`, the client must produce a Virtual DOM layout that
+exactly matches the one used for SSR including components that do not
+contain any elements. If you have any component that is only useful in
+one implementation, you may want to use a `PhantomComponent` to fill the
+position of the extra component.
+:::
+
+## Component Lifecycle during hydration
+
+During Hydration, components schedule 2 consecutive renders after it is
+created. Any effects are called after the second render completes.
+It is important to make sure that the render function of the your
+component is side-effect free. It should not mutate any states or trigger
+additional renders. If your component currently mutates states or triggers
+additional renders, move them into an `use_effect` hook.
+
+It's possible to use Struct Components with server-side rendering in
+hydration, the view function will be called
+multiple times before the rendered function will be called.
+The DOM is considered as not connected until rendered function is called,
+you should prevent any access to rendered nodes
+until `rendered()` method is called.
+
+## Example
+
+```rust ,ignore
+use yew::prelude::*;
+use yew::Renderer;
+
+#[function_component]
+fn App() -> Html {
+ html! {{"Hello, World!"}
}
+}
+
+fn main() {
+ let renderer = Renderer::::new();
+
+ // hydrates everything under body element, removes trailing
+ // elements (if any).
+ renderer.hydrate();
+}
+```
+
Example: [simple\_ssr](https://github.com/yewstack/yew/tree/master/examples/simple_ssr)
+Example: [ssr\_router](https://github.com/yewstack/yew/tree/master/examples/ssr_router)
:::caution
-Server-side rendering is experiemental and currently has no hydration support.
-However, you can still use it to generate static websites.
+Server-side rendering is currently experiemental. If you find a bug, please file
+an issue on [GitHub](https://github.com/yewstack/yew/issues/new?assignees=&labels=bug&template=bug_report.md&title=).
:::