use_prepared_state & use_transitive_state (#2650)

* Some initial implementation.

* Read prepared state during hydration.

* Decode each state with bincode.

* Feature gate prepared state.

* Update documentation.

* Switch from base64 to String.

* cargo +nightly fmt.

* Fix test.

* Add some tests.

* Minor adjustments.

* Remove unused marker.

* Update example.

* Add use_transitive_state.

* Remove unused dead code notation.

* Opt for better code size.

* Add tests for use_transitive_state.

* Fix cargo fmt.

* Fix rustdoc.

* Asynchronously decode data during hydration.

* Fix feature flags.

* Fix docs.

* Feature flags on ssr_router.

* Adjust workflow to reflect feature flags.

* Fix features.

* Restore wasm-bindgen-futures to be wasm32 only.

* Revert wasm-bindgen-futures.

* Second attempt to remove wasm-bindgen-futures.

* Remove spaces as well.

* Address reviews.

* Better diagnostic message.

* Update diagnostic messages.
This commit is contained in:
Kaede Hoshikawa 2022-05-24 13:35:16 +09:00 committed by GitHub
parent 027ab6af8b
commit b29b4535b7
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
36 changed files with 1555 additions and 78 deletions

View File

@ -25,7 +25,7 @@ jobs:
uses: actions-rs/cargo@v1
with:
command: clippy
args: --all-targets -- -D warnings
args: --all-targets --all-features -- -D warnings
- name: Lint feature soundness
run: |
@ -51,12 +51,11 @@ jobs:
- uses: Swatinem/rust-cache@v1
- name: Run clippy
uses: actions-rs/cargo@v1
with:
command: clippy
args: --all-targets --release -- -D warnings
args: --all-targets --all-features --release -- -D warnings
- name: Lint feature soundness
run: |
@ -108,7 +107,7 @@ jobs:
uses: actions-rs/cargo@v1
with:
command: test
args: --doc --workspace --exclude yew --exclude changelog --exclude website-test --target wasm32-unknown-unknown
args: --doc --workspace --exclude yew --exclude changelog --exclude website-test --exclude ssr_router --exclude simple_ssr --target wasm32-unknown-unknown
- name: Run website code snippet tests
uses: actions-rs/cargo@v1
@ -196,7 +195,7 @@ jobs:
uses: actions-rs/cargo@v1
with:
command: test
args: --all-targets --workspace --exclude yew --exclude website-test
args: --all-targets --workspace --exclude yew --exclude website-test --exclude ssr_router --exclude simple_ssr
- name: Run native tests for yew
uses: actions-rs/cargo@v1

View File

@ -6,7 +6,7 @@ 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"] }
yew = { path = "../../packages/yew" }
reqwest = { version = "0.11.8", features = ["json"] }
serde = { version = "1.0.132", features = ["derive"] }
uuid = { version = "1.0.0", features = ["serde"] }
@ -23,3 +23,7 @@ num_cpus = "1.13"
tokio-util = { version = "0.7", features = ["rt"] }
once_cell = "1.5"
clap = { version = "3.1.7", features = ["derive"] }
[features]
hydration = ["yew/hydration"]
ssr = ["yew/ssr", "yew/tokio"]

View File

@ -10,7 +10,7 @@ This example demonstrates server-side rendering.
2. Run the server
`cargo run --bin simple_ssr_server -- --dir examples/simple_ssr/dist`
`cargo run --features=ssr --bin simple_ssr_server -- --dir examples/simple_ssr/dist`
3. Open Browser

View File

@ -4,6 +4,6 @@
<meta charset="utf-8" />
<title>Yew SSR Example</title>
<link data-trunk rel="rust" data-bin="simple_ssr_hydrate" />
<link data-trunk rel="rust" data-bin="simple_ssr_hydrate" data-cargo-features="hydration" />
</head>
</html>

View File

@ -1,20 +1,13 @@
use std::cell::RefCell;
use std::rc::Rc;
use serde::{Deserialize, Serialize};
#[cfg(not(target_arch = "wasm32"))]
use tokio::task::spawn_local;
use uuid::Uuid;
#[cfg(target_arch = "wasm32")]
use wasm_bindgen_futures::spawn_local;
use yew::prelude::*;
use yew::suspense::{Suspension, SuspensionResult};
#[derive(Serialize, Deserialize)]
struct UuidResponse {
uuid: Uuid,
}
#[cfg(feature = "ssr")]
async fn fetch_uuid() -> Uuid {
// reqwest works for both non-wasm and wasm targets.
let resp = reqwest::get("https://httpbin.org/uuid").await.unwrap();
@ -23,56 +16,9 @@ async fn fetch_uuid() -> Uuid {
uuid_resp.uuid
}
pub struct UuidState {
s: Suspension,
value: Rc<RefCell<Option<Uuid>>>,
}
impl UuidState {
fn new() -> Self {
let (s, handle) = Suspension::new();
let value: Rc<RefCell<Option<Uuid>>> = Rc::default();
{
let value = value.clone();
// we use tokio spawn local here.
spawn_local(async move {
let uuid = fetch_uuid().await;
{
let mut value = value.borrow_mut();
*value = Some(uuid);
}
handle.resume();
});
}
Self { s, value }
}
}
impl PartialEq for UuidState {
fn eq(&self, rhs: &Self) -> bool {
self.s == rhs.s
}
}
#[hook]
fn use_random_uuid() -> SuspensionResult<Uuid> {
let s = use_state(UuidState::new);
let result = match *s.value.borrow() {
Some(ref m) => Ok(*m),
None => Err(s.s.clone()),
};
result
}
#[function_component]
fn Content() -> HtmlResult {
let uuid = use_random_uuid()?;
let uuid = use_prepared_state!(async |_| -> Uuid { fetch_uuid().await }, ())?.unwrap();
Ok(html! {
<div>{"Random UUID: "}{uuid}</div>

View File

@ -6,7 +6,7 @@ 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"] }
yew = { path = "../../packages/yew" }
function_router = { path = "../function_router" }
log = "0.4"
@ -24,3 +24,7 @@ num_cpus = "1.13"
tokio-util = { version = "0.7", features = ["rt"] }
once_cell = "1.5"
clap = { version = "3.1.7", features = ["derive"] }
[features]
ssr = ["yew/ssr"]
hydration = ["yew/hydration"]

View File

@ -12,7 +12,7 @@ of the function router example.
2. Run the server
`cargo run --bin ssr_router_server -- --dir examples/ssr_router/dist`
`cargo run --features=ssr --bin ssr_router_server -- --dir examples/ssr_router/dist`
3. Open Browser

View File

@ -3,7 +3,7 @@
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<link data-trunk rel="rust" data-bin="ssr_router_hydrate" />
<link data-trunk rel="rust" data-bin="ssr_router_hydrate" data-cargo-features="hydration" />
<title>Yew • SSR Router</title>
<link

View File

@ -319,6 +319,11 @@ impl FunctionComponent {
fn destroy(&mut self, _ctx: &::yew::html::Context<Self>) {
::yew::functional::FunctionComponent::<Self>::destroy(&self.function_component)
}
#[inline]
fn prepare_state(&self) -> ::std::option::Option<::std::string::String> {
::yew::functional::FunctionComponent::<Self>::prepare_state(&self.function_component)
}
}
}
}

View File

@ -53,6 +53,8 @@ mod hook;
mod html_tree;
mod props;
mod stringify;
mod use_prepared_state;
mod use_transitive_state;
use derive_props::DerivePropsInput;
use function_component::{function_component_impl, FunctionComponent, FunctionComponentName};
@ -62,6 +64,8 @@ use proc_macro::TokenStream;
use quote::ToTokens;
use syn::buffer::Cursor;
use syn::parse_macro_input;
use use_prepared_state::PreparedState;
use use_transitive_state::TransitiveState;
trait Peek<'a, T> {
fn peek(cursor: Cursor<'a>) -> Option<(T, Cursor<'a>)>;
@ -150,3 +154,27 @@ pub fn hook(attr: TokenStream, item: TokenStream) -> proc_macro::TokenStream {
.unwrap_or_else(|err| err.to_compile_error())
.into()
}
#[proc_macro]
pub fn use_prepared_state_with_closure(input: TokenStream) -> TokenStream {
let prepared_state = parse_macro_input!(input as PreparedState);
prepared_state.to_token_stream_with_closure().into()
}
#[proc_macro]
pub fn use_prepared_state_without_closure(input: TokenStream) -> TokenStream {
let prepared_state = parse_macro_input!(input as PreparedState);
prepared_state.to_token_stream_without_closure().into()
}
#[proc_macro]
pub fn use_transitive_state_with_closure(input: TokenStream) -> TokenStream {
let transitive_state = parse_macro_input!(input as TransitiveState);
transitive_state.to_token_stream_with_closure().into()
}
#[proc_macro]
pub fn use_transitive_state_without_closure(input: TokenStream) -> TokenStream {
let transitive_state = parse_macro_input!(input as TransitiveState);
transitive_state.to_token_stream_without_closure().into()
}

View File

@ -0,0 +1,116 @@
use proc_macro2::TokenStream;
use quote::quote;
use syn::parse::{Parse, ParseStream};
use syn::{parse_quote, Expr, ExprClosure, ReturnType, Token, Type};
#[derive(Debug)]
pub struct PreparedState {
closure: ExprClosure,
return_type: Type,
deps: Expr,
}
impl Parse for PreparedState {
fn parse(input: ParseStream) -> syn::Result<Self> {
// Reads a closure.
let expr: Expr = input.parse()?;
let closure = match expr {
Expr::Closure(m) => m,
other => return Err(syn::Error::new_spanned(other, "expected closure")),
};
input.parse::<Token![,]>().map_err(|e| {
syn::Error::new(
e.span(),
"this hook takes 2 arguments but 1 argument was supplied",
)
})?;
let return_type = match &closure.output {
ReturnType::Default => {
return Err(syn::Error::new_spanned(
&closure,
"You must specify a return type for this closure. This is used when the \
closure is omitted from the client side rendering bundle.",
))
}
ReturnType::Type(_rarrow, ty) => *ty.to_owned(),
};
// Reads the deps.
let deps = input.parse()?;
if !input.is_empty() {
let maybe_trailing_comma = input.lookahead1();
if !maybe_trailing_comma.peek(Token![,]) {
return Err(maybe_trailing_comma.error());
}
}
Ok(Self {
closure,
return_type,
deps,
})
}
}
impl PreparedState {
// Async closure is not stable, so we rewrite it to clsoure + async block
pub fn rewrite_to_closure_with_async_block(&self) -> ExprClosure {
let async_token = match &self.closure.asyncness {
Some(m) => m,
None => return self.closure.clone(),
};
let move_token = &self.closure.capture;
let body = &self.closure.body;
let inner = parse_quote! {
#async_token #move_token {
#body
}
};
let mut closure = self.closure.clone();
closure.asyncness = None;
// We omit the output type as it's an opaque future type.
closure.output = ReturnType::Default;
closure.body = inner;
closure.attrs.push(parse_quote! { #[allow(unused_braces)] });
closure
}
pub fn to_token_stream_with_closure(&self) -> TokenStream {
let deps = &self.deps;
let rt = &self.return_type;
let closure = self.rewrite_to_closure_with_async_block();
match &self.closure.asyncness {
Some(_) => quote! {
::yew::functional::use_prepared_state_with_suspension::<#rt, _, _, _>(#closure, #deps)
},
None => quote! {
::yew::functional::use_prepared_state::<#rt, _, _>(#closure, #deps)
},
}
}
// Expose a hook for the client side.
//
// The closure is stripped from the client side.
pub fn to_token_stream_without_closure(&self) -> TokenStream {
let deps = &self.deps;
let rt = &self.return_type;
quote! {
::yew::functional::use_prepared_state::<#rt, _>(#deps)
}
}
}

View File

@ -0,0 +1,82 @@
use proc_macro2::TokenStream;
use quote::quote;
use syn::parse::{Parse, ParseStream};
use syn::{Expr, ExprClosure, ReturnType, Token, Type};
#[derive(Debug)]
pub struct TransitiveState {
closure: ExprClosure,
return_type: Type,
deps: Expr,
}
impl Parse for TransitiveState {
fn parse(input: ParseStream) -> syn::Result<Self> {
// Reads a closure.
let expr: Expr = input.parse()?;
let closure = match expr {
Expr::Closure(m) => m,
other => return Err(syn::Error::new_spanned(other, "expected closure")),
};
input.parse::<Token![,]>().map_err(|e| {
syn::Error::new(
e.span(),
"this hook takes 2 arguments but 1 argument was supplied",
)
})?;
let return_type = match &closure.output {
ReturnType::Default => {
return Err(syn::Error::new_spanned(
&closure,
"You must specify a return type for this closure. This is used when the \
closure is omitted from the client side rendering bundle.",
))
}
ReturnType::Type(_rarrow, ty) => *ty.to_owned(),
};
// Reads the deps.
let deps = input.parse()?;
if !input.is_empty() {
let maybe_trailing_comma = input.lookahead1();
if !maybe_trailing_comma.peek(Token![,]) {
return Err(maybe_trailing_comma.error());
}
}
Ok(Self {
closure,
return_type,
deps,
})
}
}
impl TransitiveState {
pub fn to_token_stream_with_closure(&self) -> TokenStream {
let deps = &self.deps;
let rt = &self.return_type;
let closure = &self.closure;
quote! {
::yew::functional::use_transitive_state::<#rt, _, _>(#closure, #deps)
}
}
// Expose a hook for the client side.
//
// The closure is stripped from the client side.
pub fn to_token_stream_without_closure(&self) -> TokenStream {
let deps = &self.deps;
let rt = &self.return_type;
quote! {
::yew::functional::use_transitive_state::<#rt, _>(#deps)
}
}
}

View File

@ -0,0 +1,30 @@
use yew::prelude::*;
use yew_macro::{use_prepared_state_with_closure, use_prepared_state_without_closure};
#[function_component]
fn Comp() -> HtmlResult {
use_prepared_state_with_closure!(123)?;
use_prepared_state_with_closure!(|_| { todo!() }, 123)?;
use_prepared_state_with_closure!(|_| -> u32 { todo!() })?;
use_prepared_state_with_closure!(async |_| -> u32 { todo!() })?;
Ok(Html::default())
}
#[function_component]
fn Comp2() -> HtmlResult {
use_prepared_state_without_closure!(123)?;
use_prepared_state_without_closure!(|_| { todo!() }, 123)?;
use_prepared_state_without_closure!(|_| -> u32 { todo!() })?;
use_prepared_state_without_closure!(async |_| -> u32 { todo!() })?;
Ok(Html::default())
}
fn main() {}

View File

@ -0,0 +1,55 @@
error: expected closure
--> tests/hook_macro/use_prepared_state-fail.rs:6:38
|
6 | use_prepared_state_with_closure!(123)?;
| ^^^
error: You must specify a return type for this closure. This is used when the closure is omitted from the client side rendering bundle.
--> tests/hook_macro/use_prepared_state-fail.rs:8:38
|
8 | use_prepared_state_with_closure!(|_| { todo!() }, 123)?;
| ^^^^^^^^^^^^^^^
error: this hook takes 2 arguments but 1 argument was supplied
--> tests/hook_macro/use_prepared_state-fail.rs:10:5
|
10 | use_prepared_state_with_closure!(|_| -> u32 { todo!() })?;
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
|
= note: this error originates in the macro `use_prepared_state_with_closure` (in Nightly builds, run with -Z macro-backtrace for more info)
error: this hook takes 2 arguments but 1 argument was supplied
--> tests/hook_macro/use_prepared_state-fail.rs:12:5
|
12 | use_prepared_state_with_closure!(async |_| -> u32 { todo!() })?;
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
|
= note: this error originates in the macro `use_prepared_state_with_closure` (in Nightly builds, run with -Z macro-backtrace for more info)
error: expected closure
--> tests/hook_macro/use_prepared_state-fail.rs:19:41
|
19 | use_prepared_state_without_closure!(123)?;
| ^^^
error: You must specify a return type for this closure. This is used when the closure is omitted from the client side rendering bundle.
--> tests/hook_macro/use_prepared_state-fail.rs:21:41
|
21 | use_prepared_state_without_closure!(|_| { todo!() }, 123)?;
| ^^^^^^^^^^^^^^^
error: this hook takes 2 arguments but 1 argument was supplied
--> tests/hook_macro/use_prepared_state-fail.rs:23:5
|
23 | use_prepared_state_without_closure!(|_| -> u32 { todo!() })?;
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
|
= note: this error originates in the macro `use_prepared_state_without_closure` (in Nightly builds, run with -Z macro-backtrace for more info)
error: this hook takes 2 arguments but 1 argument was supplied
--> tests/hook_macro/use_prepared_state-fail.rs:25:5
|
25 | use_prepared_state_without_closure!(async |_| -> u32 { todo!() })?;
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
|
= note: this error originates in the macro `use_prepared_state_without_closure` (in Nightly builds, run with -Z macro-backtrace for more info)

View File

@ -0,0 +1,26 @@
use yew::prelude::*;
use yew_macro::{use_transitive_state_with_closure, use_transitive_state_without_closure};
#[function_component]
fn Comp() -> HtmlResult {
use_transitive_state_with_closure!(123)?;
use_transitive_state_with_closure!(|_| { todo!() }, 123)?;
use_transitive_state_with_closure!(|_| -> u32 { todo!() })?;
Ok(Html::default())
}
#[function_component]
fn Comp2() -> HtmlResult {
use_transitive_state_without_closure!(123)?;
use_transitive_state_without_closure!(|_| { todo!() }, 123)?;
use_transitive_state_without_closure!(|_| -> u32 { todo!() })?;
Ok(Html::default())
}
fn main() {}

View File

@ -0,0 +1,39 @@
error: expected closure
--> tests/hook_macro/use_transitive_state-fail.rs:6:40
|
6 | use_transitive_state_with_closure!(123)?;
| ^^^
error: You must specify a return type for this closure. This is used when the closure is omitted from the client side rendering bundle.
--> tests/hook_macro/use_transitive_state-fail.rs:8:40
|
8 | use_transitive_state_with_closure!(|_| { todo!() }, 123)?;
| ^^^^^^^^^^^^^^^
error: this hook takes 2 arguments but 1 argument was supplied
--> tests/hook_macro/use_transitive_state-fail.rs:10:5
|
10 | use_transitive_state_with_closure!(|_| -> u32 { todo!() })?;
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
|
= note: this error originates in the macro `use_transitive_state_with_closure` (in Nightly builds, run with -Z macro-backtrace for more info)
error: expected closure
--> tests/hook_macro/use_transitive_state-fail.rs:17:43
|
17 | use_transitive_state_without_closure!(123)?;
| ^^^
error: You must specify a return type for this closure. This is used when the closure is omitted from the client side rendering bundle.
--> tests/hook_macro/use_transitive_state-fail.rs:19:43
|
19 | use_transitive_state_without_closure!(|_| { todo!() }, 123)?;
| ^^^^^^^^^^^^^^^
error: this hook takes 2 arguments but 1 argument was supplied
--> tests/hook_macro/use_transitive_state-fail.rs:21:5
|
21 | use_transitive_state_without_closure!(|_| -> u32 { todo!() })?;
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
|
= note: this error originates in the macro `use_transitive_state_without_closure` (in Nightly builds, run with -Z macro-backtrace for more info)

View File

@ -0,0 +1,7 @@
#[allow(dead_code)]
#[rustversion::attr(stable(1.56), test)]
fn tests() {
let t = trybuild::TestCases::new();
t.pass("tests/hook_macro/*-pass.rs");
t.compile_fail("tests/hook_macro/*-fail.rs");
}

View File

@ -29,6 +29,9 @@ thiserror = "1.0"
futures = { version = "0.3", optional = true }
html-escape = { version = "0.2.9", optional = true }
base64ct = { version = "1.5.0", features = ["std"], optional = true }
bincode = { version = "1.3.3", optional = true }
serde = { version = "1", features = ["derive"] }
[dependencies.web-sys]
version = "0.3"
@ -61,6 +64,7 @@ features = [
"UiEvent",
"WheelEvent",
"Window",
"HtmlScriptElement",
]
[target.'cfg(target_arch = "wasm32")'.dependencies]
@ -89,9 +93,9 @@ features = [
[features]
# TODO: `dep:` syntax only supported with MSRV 1.60, would be more precise
# tokio = ["dep:tokio"]
ssr = ["futures", "html-escape"] # dep:html-escape
ssr = ["futures", "html-escape", "base64ct", "bincode"] # dep:html-escape
csr = []
hydration = ["csr"]
hydration = ["csr", "bincode"]
default = []
[target.'cfg(not(target_arch = "wasm32"))'.dev-dependencies]

View File

@ -312,7 +312,7 @@ mod tests {
M: Mixin + Properties + Default,
{
// Remove any existing elements
let body = document().body().unwrap();
let body = document().query_selector("#output").unwrap().unwrap();
while let Some(child) = body.query_selector("div#testroot").unwrap() {
body.remove_child(&child).unwrap();
}

View File

@ -3,18 +3,30 @@ mod use_context;
mod use_effect;
mod use_force_update;
mod use_memo;
#[cfg_attr(documenting, doc(cfg(any(target_arch = "wasm32", feature = "tokio"))))]
#[cfg(any(target_arch = "wasm32", feature = "tokio"))]
mod use_prepared_state;
mod use_reducer;
mod use_ref;
mod use_state;
#[cfg_attr(documenting, doc(cfg(any(target_arch = "wasm32", feature = "tokio"))))]
#[cfg(any(target_arch = "wasm32", feature = "tokio"))]
mod use_transitive_state;
pub use use_callback::*;
pub use use_context::*;
pub use use_effect::*;
pub use use_force_update::*;
pub use use_memo::*;
#[cfg_attr(documenting, doc(cfg(any(target_arch = "wasm32", feature = "tokio"))))]
#[cfg(any(target_arch = "wasm32", feature = "tokio"))]
pub use use_prepared_state::*;
pub use use_reducer::*;
pub use use_ref::*;
pub use use_state::*;
#[cfg_attr(documenting, doc(cfg(any(target_arch = "wasm32", feature = "tokio"))))]
#[cfg(any(target_arch = "wasm32", feature = "tokio"))]
pub use use_transitive_state::*;
use crate::functional::HookContext;

View File

@ -0,0 +1,122 @@
//! The client-side rendering variant. This is used for client side rendering.
use std::marker::PhantomData;
use std::rc::Rc;
use serde::de::DeserializeOwned;
use serde::Serialize;
use wasm_bindgen::JsValue;
use super::PreparedStateBase;
use crate::functional::{use_state, Hook, HookContext};
use crate::io_coop::spawn_local;
use crate::suspense::{Suspension, SuspensionResult};
#[cfg(target_arch = "wasm32")]
async fn decode_base64(s: &str) -> Result<Vec<u8>, JsValue> {
use gloo_utils::window;
use js_sys::Uint8Array;
use wasm_bindgen::JsCast;
use wasm_bindgen_futures::JsFuture;
let fetch_promise = window().fetch_with_str(s);
let content_promise = JsFuture::from(fetch_promise)
.await
.and_then(|m| m.dyn_into::<web_sys::Response>())
.and_then(|m| m.array_buffer())?;
let content_array = JsFuture::from(content_promise)
.await
.as_ref()
.map(Uint8Array::new)?;
Ok(content_array.to_vec())
}
#[cfg(not(target_arch = "wasm32"))]
async fn decode_base64(_s: &str) -> Result<Vec<u8>, JsValue> {
unreachable!("this function is not callable under non-wasm targets!");
}
#[doc(hidden)]
pub fn use_prepared_state<T, D>(deps: D) -> impl Hook<Output = SuspensionResult<Option<Rc<T>>>>
where
D: Serialize + DeserializeOwned + PartialEq + 'static,
T: Serialize + DeserializeOwned + 'static,
{
struct HookProvider<T, D>
where
D: Serialize + DeserializeOwned + PartialEq + 'static,
T: Serialize + DeserializeOwned + 'static,
{
_marker: PhantomData<T>,
deps: D,
}
impl<T, D> Hook for HookProvider<T, D>
where
D: Serialize + DeserializeOwned + PartialEq + 'static,
T: Serialize + DeserializeOwned + 'static,
{
type Output = SuspensionResult<Option<Rc<T>>>;
fn run(self, ctx: &mut HookContext) -> Self::Output {
let data = use_state(|| {
let (s, handle) = Suspension::new();
(
SuspensionResult::<(Option<Rc<T>>, Option<Rc<D>>)>::Err(s),
Some(handle),
)
})
.run(ctx);
let state = {
let data = data.clone();
ctx.next_prepared_state(move |_re_render, buf| -> PreparedStateBase<T, D> {
if let Some(buf) = buf {
let buf = format!("data:application/octet-binary;base64,{}", buf);
spawn_local(async move {
let buf = decode_base64(&buf)
.await
.expect("failed to deserialize state");
let (state, deps) =
bincode::deserialize::<(Option<T>, Option<D>)>(&buf)
.map(|(state, deps)| (state.map(Rc::new), deps.map(Rc::new)))
.expect("failed to deserialize state");
data.set((Ok((state, deps)), None));
});
}
PreparedStateBase {
#[cfg(feature = "ssr")]
state: None,
#[cfg(feature = "ssr")]
deps: None,
has_buf: buf.is_some(),
_marker: PhantomData,
}
})
};
if state.has_buf {
let (data, deps) = data.0.clone()?;
if deps.as_deref() == Some(&self.deps) {
return Ok(data);
}
}
Ok(None)
}
}
HookProvider::<T, D> {
_marker: PhantomData,
deps,
}
}

View File

@ -0,0 +1,95 @@
//! The client-and-server-side rendering variant.
use std::future::Future;
use std::rc::Rc;
use serde::de::DeserializeOwned;
use serde::Serialize;
use super::{feat_hydration, feat_ssr};
use crate::functional::{Hook, HookContext};
use crate::html::RenderMode;
use crate::suspense::SuspensionResult;
#[doc(hidden)]
pub fn use_prepared_state<T, D, F>(
f: F,
deps: D,
) -> impl Hook<Output = SuspensionResult<Option<Rc<T>>>>
where
D: Serialize + DeserializeOwned + PartialEq + 'static,
T: Serialize + DeserializeOwned + 'static,
F: FnOnce(&D) -> T,
{
struct HookProvider<T, D, F>
where
D: Serialize + DeserializeOwned + PartialEq + 'static,
T: Serialize + DeserializeOwned + 'static,
F: FnOnce(&D) -> T,
{
deps: D,
f: F,
}
impl<T, D, F> Hook for HookProvider<T, D, F>
where
D: Serialize + DeserializeOwned + PartialEq + 'static,
T: Serialize + DeserializeOwned + 'static,
F: FnOnce(&D) -> T,
{
type Output = SuspensionResult<Option<Rc<T>>>;
fn run(self, ctx: &mut HookContext) -> Self::Output {
match ctx.mode {
RenderMode::Ssr => feat_ssr::use_prepared_state(self.f, self.deps).run(ctx),
_ => feat_hydration::use_prepared_state(self.deps).run(ctx),
}
}
}
HookProvider::<T, D, F> { deps, f }
}
#[doc(hidden)]
pub fn use_prepared_state_with_suspension<T, D, F, U>(
f: F,
deps: D,
) -> impl Hook<Output = SuspensionResult<Option<Rc<T>>>>
where
D: Serialize + DeserializeOwned + PartialEq + 'static,
T: Serialize + DeserializeOwned + 'static,
F: FnOnce(&D) -> U,
U: 'static + Future<Output = T>,
{
struct HookProvider<T, D, F, U>
where
D: Serialize + DeserializeOwned + PartialEq + 'static,
T: Serialize + DeserializeOwned + 'static,
F: FnOnce(&D) -> U,
U: 'static + Future<Output = T>,
{
deps: D,
f: F,
}
impl<T, D, F, U> Hook for HookProvider<T, D, F, U>
where
D: Serialize + DeserializeOwned + PartialEq + 'static,
T: Serialize + DeserializeOwned + 'static,
F: FnOnce(&D) -> U,
U: 'static + Future<Output = T>,
{
type Output = SuspensionResult<Option<Rc<T>>>;
fn run(self, ctx: &mut HookContext) -> Self::Output {
match ctx.mode {
RenderMode::Ssr => {
feat_ssr::use_prepared_state_with_suspension(self.f, self.deps).run(ctx)
}
_ => feat_hydration::use_prepared_state(self.deps).run(ctx),
}
}
}
HookProvider::<T, D, F, U> { deps, f }
}

View File

@ -0,0 +1,29 @@
//! The noop variant. This is used for client side rendering when hydration is disabled.
use std::rc::Rc;
use serde::de::DeserializeOwned;
use serde::Serialize;
use crate::hook;
use crate::suspense::SuspensionResult;
#[doc(hidden)]
#[hook]
pub fn use_prepared_state<T, D>(_deps: D) -> SuspensionResult<Option<Rc<T>>>
where
D: Serialize + DeserializeOwned + PartialEq + 'static,
T: Serialize + DeserializeOwned + 'static,
{
Ok(None)
}
#[doc(hidden)]
#[hook]
pub fn use_prepared_state_with_suspension<T, D>(_deps: D) -> SuspensionResult<Option<Rc<T>>>
where
D: Serialize + DeserializeOwned + PartialEq + 'static,
T: Serialize + DeserializeOwned + 'static,
{
Ok(None)
}

View File

@ -0,0 +1,143 @@
//! The server-side rendering variant. This is used for server side rendering.
use std::future::Future;
use std::marker::PhantomData;
use std::rc::Rc;
use serde::de::DeserializeOwned;
use serde::Serialize;
use super::PreparedStateBase;
use crate::functional::{use_memo, use_state, Hook, HookContext};
use crate::io_coop::spawn_local;
use crate::suspense::{Suspension, SuspensionResult};
#[doc(hidden)]
pub fn use_prepared_state<T, D, F>(
f: F,
deps: D,
) -> impl Hook<Output = SuspensionResult<Option<Rc<T>>>>
where
D: Serialize + DeserializeOwned + PartialEq + 'static,
T: Serialize + DeserializeOwned + 'static,
F: FnOnce(&D) -> T,
{
struct HookProvider<T, D, F>
where
D: Serialize + DeserializeOwned + PartialEq + 'static,
T: Serialize + DeserializeOwned + 'static,
F: FnOnce(&D) -> T,
{
deps: D,
f: F,
}
impl<T, D, F> Hook for HookProvider<T, D, F>
where
D: Serialize + DeserializeOwned + PartialEq + 'static,
T: Serialize + DeserializeOwned + 'static,
F: FnOnce(&D) -> T,
{
type Output = SuspensionResult<Option<Rc<T>>>;
fn run(self, ctx: &mut HookContext) -> Self::Output {
let f = self.f;
let deps = Rc::new(self.deps);
let state = {
let deps = deps.clone();
use_memo(move |_| f(&deps), ()).run(ctx)
};
let state = PreparedStateBase {
state: Some(state),
deps: Some(deps),
#[cfg(feature = "hydration")]
has_buf: true,
_marker: PhantomData,
};
let state =
ctx.next_prepared_state(|_re_render, _| -> PreparedStateBase<T, D> { state });
Ok(state.state.clone())
}
}
HookProvider::<T, D, F> { deps, f }
}
#[doc(hidden)]
pub fn use_prepared_state_with_suspension<T, D, F, U>(
f: F,
deps: D,
) -> impl Hook<Output = SuspensionResult<Option<Rc<T>>>>
where
D: Serialize + DeserializeOwned + PartialEq + 'static,
T: Serialize + DeserializeOwned + 'static,
F: FnOnce(&D) -> U,
U: 'static + Future<Output = T>,
{
struct HookProvider<T, D, F, U>
where
D: Serialize + DeserializeOwned + PartialEq + 'static,
T: Serialize + DeserializeOwned + 'static,
F: FnOnce(&D) -> U,
U: 'static + Future<Output = T>,
{
deps: D,
f: F,
}
impl<T, D, F, U> Hook for HookProvider<T, D, F, U>
where
D: Serialize + DeserializeOwned + PartialEq + 'static,
T: Serialize + DeserializeOwned + 'static,
F: FnOnce(&D) -> U,
U: 'static + Future<Output = T>,
{
type Output = SuspensionResult<Option<Rc<T>>>;
fn run(self, ctx: &mut HookContext) -> Self::Output {
let f = self.f;
let deps = Rc::new(self.deps);
let result = use_state(|| {
let (s, handle) = Suspension::new();
(Err(s), Some(handle))
})
.run(ctx);
{
let deps = deps.clone();
let result = result.clone();
use_state(move || {
let state_f = f(&deps);
spawn_local(async move {
let state = state_f.await;
result.set((Ok(Rc::new(state)), None));
})
})
.run(ctx);
}
let state = result.0.clone()?;
let state = PreparedStateBase {
state: Some(state),
deps: Some(deps),
#[cfg(feature = "hydration")]
has_buf: true,
_marker: PhantomData,
};
let state =
ctx.next_prepared_state(|_re_render, _| -> PreparedStateBase<T, D> { state });
Ok(state.state.clone())
}
}
HookProvider::<T, D, F, U> { deps, f }
}

View File

@ -0,0 +1,146 @@
#[cfg(feature = "hydration")]
pub(super) mod feat_hydration;
#[cfg(all(feature = "hydration", feature = "ssr"))]
mod feat_hydration_ssr;
#[cfg(not(any(feature = "hydration", feature = "ssr")))]
pub(super) mod feat_none;
#[cfg(feature = "ssr")]
mod feat_ssr;
#[cfg(all(feature = "hydration", not(feature = "ssr")))]
pub use feat_hydration::*;
#[cfg(all(feature = "ssr", feature = "hydration"))]
pub use feat_hydration_ssr::*;
#[cfg(not(any(feature = "hydration", feature = "ssr")))]
pub use feat_none::*;
#[cfg(all(feature = "ssr", not(feature = "hydration")))]
pub use feat_ssr::*;
/// Use a state prepared on the server side and its value is sent to the client side during
/// hydration.
///
/// The component sees the same value on the server side and client side if the component is
/// hydrated.
///
/// It accepts a closure as the first argument and a dependency type as the second argument.
/// It returns `SuspensionResult<Option<Rc<T>>>`.
///
/// During hydration, it will only return `Ok(Some(Rc<T>))` if the component is hydrated from a
/// server-side rendering artifact and its dependency value matches.
///
/// `let state = use_prepared_state!(|deps| -> ReturnType { ... }, deps)?;`
///
/// It has the following signature:
///
/// ```
/// # use serde::de::DeserializeOwned;
/// # use serde::Serialize;
/// # use std::rc::Rc;
/// use yew::prelude::*;
/// use yew::suspense::SuspensionResult;
///
/// #[hook]
/// pub fn use_prepared_state<T, D, F>(f: F, deps: D) -> SuspensionResult<Option<Rc<T>>>
/// where
/// D: Serialize + DeserializeOwned + PartialEq + 'static,
/// T: Serialize + DeserializeOwned + 'static,
/// F: FnOnce(&D) -> T,
/// # { todo!() }
/// ```
///
/// The first argument can also be an [async closure](https://github.com/rust-lang/rust/issues/62290).
///
/// `let state = use_prepared_state!(async |deps| -> ReturnType { ... }, deps)?;`
///
/// When accepting an async closure, it has the following signature:
///
/// ```
/// # use serde::de::DeserializeOwned;
/// # use serde::Serialize;
/// # use std::rc::Rc;
/// # use std::future::Future;
/// use yew::prelude::*;
/// use yew::suspense::SuspensionResult;
///
/// #[hook]
/// pub fn use_prepared_state<T, D, F, U>(
/// f: F,
/// deps: D,
/// ) -> SuspensionResult<Option<Rc<T>>>
/// where
/// D: Serialize + DeserializeOwned + PartialEq + 'static,
/// T: Serialize + DeserializeOwned + 'static,
/// F: FnOnce(&D) -> U,
/// U: 'static + Future<Output = T>,
/// # { todo!() }
/// ```
///
/// During server-side rendering, a value of type `T` will be calculated from the first
/// closure.
///
/// If the bundle is compiled without server-side rendering, the closure will be stripped
/// automatically.
///
/// # Note
///
/// You MUST denote the return type of the closure with `|deps| -> ReturnType { ... }`. This
/// type is used during client side rendering to deserialize the state prepared on the server
/// side.
///
/// Whilst async closure is an unstable feature, the procedural macro will rewrite this to a
/// closure that returns an async block automatically. You can use this hook with async closure
/// in stable Rust.
#[cfg_attr(documenting, doc(cfg(any(target_arch = "wasm32", feature = "tokio"))))]
pub use use_prepared_state_macro as use_prepared_state;
// With SSR.
#[doc(hidden)]
#[cfg(feature = "ssr")]
pub use yew_macro::use_prepared_state_with_closure as use_prepared_state_macro;
// Without SSR.
#[doc(hidden)]
#[cfg(not(feature = "ssr"))]
pub use yew_macro::use_prepared_state_without_closure as use_prepared_state_macro;
#[cfg(any(feature = "hydration", feature = "ssr"))]
mod feat_any_hydration_ssr {
use std::marker::PhantomData;
#[cfg(feature = "ssr")]
use std::rc::Rc;
use serde::de::DeserializeOwned;
use serde::Serialize;
use crate::functional::PreparedState;
pub(super) struct PreparedStateBase<T, D>
where
D: Serialize + DeserializeOwned + PartialEq + 'static,
T: Serialize + DeserializeOwned + 'static,
{
#[cfg(feature = "ssr")]
pub state: Option<Rc<T>>,
#[cfg(feature = "ssr")]
pub deps: Option<Rc<D>>,
#[cfg(feature = "hydration")]
pub has_buf: bool,
pub _marker: PhantomData<(T, D)>,
}
impl<T, D> PreparedState for PreparedStateBase<T, D>
where
D: Serialize + DeserializeOwned + PartialEq + 'static,
T: Serialize + DeserializeOwned + 'static,
{
#[cfg(feature = "ssr")]
fn prepare(&self) -> String {
use base64ct::{Base64, Encoding};
let state = bincode::serialize(&(self.state.as_deref(), self.deps.as_deref()))
.expect("failed to prepare state");
Base64::encode_string(&state)
}
}
}
#[cfg(any(feature = "hydration", feature = "ssr"))]
use feat_any_hydration_ssr::PreparedStateBase;

View File

@ -0,0 +1,6 @@
//! The hydration variant.
//!
//! This is the same as the use_prepared_state.
#[doc(hidden)]
pub use crate::functional::hooks::use_prepared_state::feat_hydration::use_prepared_state as use_transitive_state;

View File

@ -0,0 +1,50 @@
//! The client-and-server-side rendering variant.
use std::rc::Rc;
use serde::de::DeserializeOwned;
use serde::Serialize;
use super::{feat_hydration, feat_ssr};
use crate::functional::{Hook, HookContext};
use crate::html::RenderMode;
use crate::suspense::SuspensionResult;
#[doc(hidden)]
pub fn use_transitive_state<T, D, F>(
f: F,
deps: D,
) -> impl Hook<Output = SuspensionResult<Option<Rc<T>>>>
where
D: Serialize + DeserializeOwned + PartialEq + 'static,
T: Serialize + DeserializeOwned + 'static,
F: 'static + FnOnce(&D) -> T,
{
struct HookProvider<T, D, F>
where
D: Serialize + DeserializeOwned + PartialEq + 'static,
T: Serialize + DeserializeOwned + 'static,
F: 'static + FnOnce(&D) -> T,
{
deps: D,
f: F,
}
impl<T, D, F> Hook for HookProvider<T, D, F>
where
D: Serialize + DeserializeOwned + PartialEq + 'static,
T: Serialize + DeserializeOwned + 'static,
F: 'static + FnOnce(&D) -> T,
{
type Output = SuspensionResult<Option<Rc<T>>>;
fn run(self, ctx: &mut HookContext) -> Self::Output {
match ctx.mode {
RenderMode::Ssr => feat_ssr::use_transitive_state(self.f, self.deps).run(ctx),
_ => feat_hydration::use_transitive_state(self.deps).run(ctx),
}
}
}
HookProvider::<T, D, F> { deps, f }
}

View File

@ -0,0 +1,4 @@
//! The noop variant. This is used for client side rendering when hydration is disabled.
#[doc(hidden)]
pub use crate::functional::hooks::use_prepared_state::feat_none::use_prepared_state as use_transitive_state;

View File

@ -0,0 +1,83 @@
//! The server-side rendering variant.
use std::cell::RefCell;
use std::rc::Rc;
use base64ct::{Base64, Encoding};
use serde::de::DeserializeOwned;
use serde::Serialize;
use crate::functional::{Hook, HookContext, PreparedState};
use crate::suspense::SuspensionResult;
pub(super) struct TransitiveStateBase<T, D, F>
where
D: Serialize + DeserializeOwned + PartialEq + 'static,
T: Serialize + DeserializeOwned + 'static,
F: 'static + FnOnce(&D) -> T,
{
pub state_fn: RefCell<Option<F>>,
pub deps: D,
}
impl<T, D, F> PreparedState for TransitiveStateBase<T, D, F>
where
D: Serialize + DeserializeOwned + PartialEq + 'static,
T: Serialize + DeserializeOwned + 'static,
F: 'static + FnOnce(&D) -> T,
{
fn prepare(&self) -> String {
let f = self.state_fn.borrow_mut().take().unwrap();
let state = f(&self.deps);
let state =
bincode::serialize(&(Some(&state), Some(&self.deps))).expect("failed to prepare state");
Base64::encode_string(&state)
}
}
#[doc(hidden)]
pub fn use_transitive_state<T, D, F>(
f: F,
deps: D,
) -> impl Hook<Output = SuspensionResult<Option<Rc<T>>>>
where
D: Serialize + DeserializeOwned + PartialEq + 'static,
T: Serialize + DeserializeOwned + 'static,
F: 'static + FnOnce(&D) -> T,
{
struct HookProvider<T, D, F>
where
D: Serialize + DeserializeOwned + PartialEq + 'static,
T: Serialize + DeserializeOwned + 'static,
F: 'static + FnOnce(&D) -> T,
{
deps: D,
f: F,
}
impl<T, D, F> Hook for HookProvider<T, D, F>
where
D: Serialize + DeserializeOwned + PartialEq + 'static,
T: Serialize + DeserializeOwned + 'static,
F: 'static + FnOnce(&D) -> T,
{
type Output = SuspensionResult<Option<Rc<T>>>;
fn run(self, ctx: &mut HookContext) -> Self::Output {
let f = self.f;
ctx.next_prepared_state(move |_re_render, _| -> TransitiveStateBase<T, D, F> {
TransitiveStateBase {
state_fn: Some(f).into(),
deps: self.deps,
}
});
Ok(None)
}
}
HookProvider::<T, D, F> { deps, f }
}

View File

@ -0,0 +1,67 @@
#[cfg(feature = "hydration")]
mod feat_hydration;
#[cfg(all(feature = "ssr", feature = "hydration"))]
mod feat_hydration_ssr;
#[cfg(not(any(feature = "hydration", feature = "ssr")))]
mod feat_none;
#[cfg(feature = "ssr")]
mod feat_ssr;
#[cfg(all(feature = "hydration", not(feature = "ssr")))]
pub use feat_hydration::*;
#[cfg(all(feature = "ssr", feature = "hydration"))]
pub use feat_hydration_ssr::*;
#[cfg(not(any(feature = "hydration", feature = "ssr")))]
pub use feat_none::*;
#[cfg(all(feature = "ssr", not(feature = "hydration")))]
pub use feat_ssr::*;
/// Use a state created as an artifact of the server-side rendering.
///
/// This value is created after the server-side rendering artifact is created.
///
/// It accepts a closure as the first argument and a dependency type as the second argument.
/// It returns `SuspensionResult<Option<Rc<T>>>`.
///
/// It will always return `Ok(None)` during server-side rendering.
///
/// During hydration, it will only return `Ok(Some(Rc<T>))` if the component is hydrated from a
/// server-side rendering artifact and its dependency value matches.
///
/// `let state = use_transitive_state!(|deps| -> ReturnType { ... }, deps);`
///
/// It has the following function signature:
///
/// ```
/// # use serde::de::DeserializeOwned;
/// # use serde::Serialize;
/// # use std::rc::Rc;
/// use yew::prelude::*;
/// use yew::suspense::SuspensionResult;
///
/// #[hook]
/// pub fn use_transitive_state<T, D, F>(f: F, deps: D) -> SuspensionResult<Option<Rc<T>>>
/// where
/// D: Serialize + DeserializeOwned + PartialEq + 'static,
/// T: Serialize + DeserializeOwned + 'static,
/// F: 'static + FnOnce(&D) -> T,
/// # { todo!() }
/// ```
///
/// If the bundle is compiled without server-side rendering, the closure will be stripped
/// automatically.
///
/// # Note
///
/// You MUST denote the return type of the closure with `|deps| -> ReturnType { ... }`. This
/// type is used during client side rendering to deserialize the state prepared on the server
/// side.
#[cfg_attr(documenting, doc(cfg(any(target_arch = "wasm32", feature = "tokio"))))]
pub use use_transitive_state_macro as use_transitive_state;
// With SSR.
#[doc(hidden)]
#[cfg(feature = "ssr")]
pub use yew_macro::use_transitive_state_with_closure as use_transitive_state_macro;
// Without SSR.
#[doc(hidden)]
#[cfg(not(feature = "ssr"))]
pub use yew_macro::use_transitive_state_without_closure as use_transitive_state_macro;

View File

@ -27,6 +27,9 @@ use std::rc::Rc;
use wasm_bindgen::prelude::*;
#[cfg(any(target_arch = "wasm32", feature = "tokio"))]
#[cfg(all(feature = "hydration", feature = "ssr"))]
use crate::html::RenderMode;
use crate::html::{AnyScope, BaseComponent, Context, HtmlResult};
use crate::Properties;
@ -64,7 +67,15 @@ pub use yew_macro::hook;
type ReRender = Rc<dyn Fn()>;
/// Primitives of a Hook state.
/// Primitives of a prepared state hook.
#[cfg(any(target_arch = "wasm32", feature = "tokio"))]
#[cfg(any(feature = "hydration", feature = "ssr"))]
pub(crate) trait PreparedState {
#[cfg(feature = "ssr")]
fn prepare(&self) -> String;
}
/// Primitives of an effect hook.
pub(crate) trait Effect {
fn rendered(&self) {}
}
@ -72,24 +83,68 @@ pub(crate) trait Effect {
/// A hook context to be passed to hooks.
pub struct HookContext {
pub(crate) scope: AnyScope,
#[cfg(any(target_arch = "wasm32", feature = "tokio"))]
#[cfg(all(feature = "hydration", feature = "ssr"))]
mode: RenderMode,
re_render: ReRender,
states: Vec<Rc<dyn Any>>,
effects: Vec<Rc<dyn Effect>>,
#[cfg(any(target_arch = "wasm32", feature = "tokio"))]
#[cfg(any(feature = "hydration", feature = "ssr"))]
prepared_states: Vec<Rc<dyn PreparedState>>,
#[cfg(any(target_arch = "wasm32", feature = "tokio"))]
#[cfg(feature = "hydration")]
prepared_states_data: Vec<Rc<str>>,
#[cfg(any(target_arch = "wasm32", feature = "tokio"))]
#[cfg(feature = "hydration")]
prepared_state_counter: usize,
counter: usize,
#[cfg(debug_assertions)]
total_hook_counter: Option<usize>,
}
impl HookContext {
fn new(scope: AnyScope, re_render: ReRender) -> RefCell<Self> {
fn new(
scope: AnyScope,
re_render: ReRender,
#[cfg(any(target_arch = "wasm32", feature = "tokio"))]
#[cfg(all(feature = "hydration", feature = "ssr"))]
mode: RenderMode,
#[cfg(any(target_arch = "wasm32", feature = "tokio"))]
#[cfg(feature = "hydration")]
prepared_state: Option<&str>,
) -> RefCell<Self> {
RefCell::new(HookContext {
effects: Vec::new(),
scope,
re_render,
#[cfg(any(target_arch = "wasm32", feature = "tokio"))]
#[cfg(all(feature = "hydration", feature = "ssr"))]
mode,
states: Vec::new(),
#[cfg(any(target_arch = "wasm32", feature = "tokio"))]
#[cfg(any(feature = "hydration", feature = "ssr"))]
prepared_states: Vec::new(),
effects: Vec::new(),
#[cfg(any(target_arch = "wasm32", feature = "tokio"))]
#[cfg(feature = "hydration")]
prepared_states_data: {
match prepared_state {
Some(m) => m.split(',').map(Rc::from).collect(),
None => Vec::new(),
}
},
#[cfg(any(target_arch = "wasm32", feature = "tokio"))]
#[cfg(feature = "hydration")]
prepared_state_counter: 0,
counter: 0,
#[cfg(debug_assertions)]
total_hook_counter: None,
@ -132,8 +187,45 @@ impl HookContext {
t
}
#[cfg(any(target_arch = "wasm32", feature = "tokio"))]
#[cfg(any(feature = "hydration", feature = "ssr"))]
pub(crate) fn next_prepared_state<T>(
&mut self,
initializer: impl FnOnce(ReRender, Option<&str>) -> T,
) -> Rc<T>
where
T: 'static + PreparedState,
{
#[cfg(not(feature = "hydration"))]
let prepared_state = Option::<Rc<str>>::None;
#[cfg(feature = "hydration")]
let prepared_state = {
let prepared_state_pos = self.prepared_state_counter;
self.prepared_state_counter += 1;
self.prepared_states_data.get(prepared_state_pos).cloned()
};
let prev_state_len = self.states.len();
let t = self.next_state(move |re_render| initializer(re_render, prepared_state.as_deref()));
// This is a new effect, we add it to effects.
if self.states.len() != prev_state_len {
self.prepared_states.push(t.clone());
}
t
}
#[inline(always)]
fn prepare_run(&mut self) {
#[cfg(any(target_arch = "wasm32", feature = "tokio"))]
#[cfg(feature = "hydration")]
{
self.prepared_state_counter = 0;
}
self.counter = 0;
}
@ -184,6 +276,33 @@ impl HookContext {
drop(state);
}
}
#[cfg(any(
not(feature = "ssr"),
not(any(target_arch = "wasm32", feature = "tokio"))
))]
fn prepare_state(&self) -> Option<String> {
None
}
#[cfg(any(target_arch = "wasm32", feature = "tokio"))]
#[cfg(feature = "ssr")]
fn prepare_state(&self) -> Option<String> {
if self.prepared_states.is_empty() {
return None;
}
let prepared_states = self.prepared_states.clone();
let mut states = Vec::new();
for state in prepared_states.iter() {
let state = state.prepare();
states.push(state);
}
Some(states.join(","))
}
}
impl fmt::Debug for HookContext {
@ -239,7 +358,16 @@ where
Self {
_never: std::marker::PhantomData::default(),
hook_ctx: HookContext::new(scope, re_render),
hook_ctx: HookContext::new(
scope,
re_render,
#[cfg(any(target_arch = "wasm32", feature = "tokio"))]
#[cfg(all(feature = "hydration", feature = "ssr"))]
ctx.mode(),
#[cfg(any(target_arch = "wasm32", feature = "tokio"))]
#[cfg(feature = "hydration")]
ctx.prepared_state(),
),
}
}
@ -269,6 +397,12 @@ where
let mut hook_ctx = self.hook_ctx.borrow_mut();
hook_ctx.drain_states();
}
/// Prepares the server-side state.
pub fn prepare_state(&self) -> Option<String> {
let hook_ctx = self.hook_ctx.borrow();
hook_ctx.prepare_state()
}
}
impl<T> fmt::Debug for FunctionComponent<T>

View File

@ -238,10 +238,11 @@ pub(crate) struct ComponentState {
}
impl ComponentState {
pub(crate) fn new<COMP: BaseComponent>(
fn new<COMP: BaseComponent>(
initial_render_state: ComponentRenderState,
scope: Scope<COMP>,
props: Rc<COMP::Properties>,
#[cfg(feature = "hydration")] prepared_state: Option<String>,
) -> Self {
let comp_id = scope.id;
#[cfg(feature = "hydration")]
@ -259,6 +260,8 @@ impl ComponentState {
props,
#[cfg(feature = "hydration")]
mode,
#[cfg(feature = "hydration")]
prepared_state,
};
let inner = Box::new(CompStateInner {
@ -295,6 +298,8 @@ pub(crate) struct CreateRunner<COMP: BaseComponent> {
pub initial_render_state: ComponentRenderState,
pub props: Rc<COMP::Properties>,
pub scope: Scope<COMP>,
#[cfg(feature = "hydration")]
pub prepared_state: Option<String>,
}
impl<COMP: BaseComponent> Runnable for CreateRunner<COMP> {
@ -308,6 +313,8 @@ impl<COMP: BaseComponent> Runnable for CreateRunner<COMP> {
self.initial_render_state,
self.scope.clone(),
self.props,
#[cfg(feature = "hydration")]
self.prepared_state,
));
}
}

View File

@ -75,6 +75,9 @@ pub struct Context<COMP: BaseComponent> {
props: Rc<COMP::Properties>,
#[cfg(feature = "hydration")]
mode: RenderMode,
#[cfg(feature = "hydration")]
prepared_state: Option<String>,
}
impl<COMP: BaseComponent> Context<COMP> {
@ -94,6 +97,17 @@ impl<COMP: BaseComponent> Context<COMP> {
pub(crate) fn mode(&self) -> RenderMode {
self.mode
}
/// The component's prepared state
pub fn prepared_state(&self) -> Option<&str> {
#[cfg(not(feature = "hydration"))]
let state = None;
#[cfg(feature = "hydration")]
let state = self.prepared_state.as_deref();
state
}
}
/// The common base of both function components and struct components.
@ -136,6 +150,9 @@ pub trait BaseComponent: Sized + 'static {
/// Notified before a component is destroyed.
fn destroy(&mut self, ctx: &Context<Self>);
/// Prepares the server-side state.
fn prepare_state(&self) -> Option<String>;
}
/// Components are the basic building blocks of the UI in a Yew app. Each Component
@ -195,6 +212,16 @@ pub trait Component: Sized + 'static {
#[allow(unused_variables)]
fn rendered(&mut self, ctx: &Context<Self>, first_render: bool) {}
/// Prepares the state during server side rendering.
///
/// This state will be sent to the client side and is available via `ctx.prepared_state()`.
///
/// This method is only called during server-side rendering after the component has been
/// rendered.
fn prepare_state(&self) -> Option<String> {
None
}
/// Called right before a Component is unmounted.
#[allow(unused_variables)]
fn destroy(&mut self, ctx: &Context<Self>) {}
@ -230,4 +257,8 @@ where
fn destroy(&mut self, ctx: &Context<Self>) {
Component::destroy(self, ctx)
}
fn prepare_state(&self) -> Option<String> {
Component::prepare_state(self)
}
}

View File

@ -285,6 +285,8 @@ mod feat_ssr {
initial_render_state: state,
props,
scope: self.clone(),
#[cfg(feature = "hydration")]
prepared_state: None,
}),
Box::new(RenderRunner {
state: self.state.clone(),
@ -303,6 +305,12 @@ mod feat_ssr {
let self_any_scope = AnyScope::from(self.clone());
html.render_to_string(w, &self_any_scope, hydratable).await;
if let Some(prepared_state) = self.get_component().unwrap().prepare_state() {
w.push_str(r#"<script type="application/x-yew-comp-state">"#);
w.push_str(&prepared_state);
w.push_str(r#"</script>"#);
}
if hydratable {
collectable.write_close_tag(w);
}
@ -501,7 +509,6 @@ 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,
@ -524,6 +531,8 @@ mod feat_csr {
initial_render_state: state,
props,
scope: self.clone(),
#[cfg(feature = "hydration")]
prepared_state: None,
}),
Box::new(RenderRunner {
state: self.state.clone(),
@ -596,7 +605,8 @@ pub(crate) use feat_csr::*;
#[cfg_attr(documenting, doc(cfg(feature = "hydration")))]
#[cfg(feature = "hydration")]
mod feat_hydration {
use web_sys::Element;
use wasm_bindgen::JsCast;
use web_sys::{Element, HtmlScriptElement};
use super::*;
use crate::dom_bundle::{BSubtree, Fragment};
@ -636,10 +646,23 @@ mod feat_hydration {
let collectable = Collectable::for_component::<COMP>();
let fragment = Fragment::collect_between(fragment, &collectable, &parent);
let mut fragment = Fragment::collect_between(fragment, &collectable, &parent);
node_ref.set(fragment.front().cloned());
let next_sibling = NodeRef::default();
let prepared_state = match fragment
.back()
.cloned()
.and_then(|m| m.dyn_into::<HtmlScriptElement>().ok())
{
Some(m) if m.type_() == "application/x-yew-comp-state" => {
fragment.pop_back();
parent.remove_child(&m).unwrap();
Some(m.text().unwrap())
}
_ => None,
};
let state = ComponentRenderState::Hydration {
root,
parent,
@ -654,6 +677,7 @@ mod feat_hydration {
initial_render_state: state,
props,
scope: self.clone(),
prepared_state,
}),
Box::new(RenderRunner {
state: self.state.clone(),

View File

@ -0,0 +1,114 @@
#![cfg(target_arch = "wasm32")]
#![cfg(feature = "hydration")]
use std::time::Duration;
mod common;
use common::obtain_result_by_id;
use gloo::timers::future::sleep;
use wasm_bindgen_test::*;
use yew::prelude::*;
use yew::{Renderer, ServerRenderer};
wasm_bindgen_test::wasm_bindgen_test_configure!(run_in_browser);
#[wasm_bindgen_test]
async fn use_prepared_state_works() {
#[function_component]
fn Comp() -> HtmlResult {
let ctr = use_prepared_state!(|_| -> u32 { 12345 }, ())?.unwrap_or_default();
Ok(html! {
<div>
{*ctr}
</div>
})
}
#[function_component]
fn App() -> Html {
html! {
<Suspense fallback={Html::default()}>
<div>
<Comp />
</div>
</Suspense>
}
}
let s = ServerRenderer::<App>::new().render().await;
assert_eq!(
s,
r#"<!--<[use_prepared_state::use_prepared_state_works::{{closure}}::App]>--><!--<[yew::suspense::component::feat_csr_ssr::Suspense]>--><!--<[yew::suspense::component::feat_csr_ssr::BaseSuspense]>--><!--<?>--><div><!--<[use_prepared_state::use_prepared_state_works::{{closure}}::Comp]>--><div>12345</div><script type="application/x-yew-comp-state">ATkwAAAB</script><!--</[use_prepared_state::use_prepared_state_works::{{closure}}::Comp]>--></div><!--</?>--><!--</[yew::suspense::component::feat_csr_ssr::BaseSuspense]>--><!--</[yew::suspense::component::feat_csr_ssr::Suspense]>--><!--</[use_prepared_state::use_prepared_state_works::{{closure}}::App]>-->"#
);
gloo::utils::document()
.query_selector("#output")
.unwrap()
.unwrap()
.set_inner_html(&s);
sleep(Duration::ZERO).await;
Renderer::<App>::with_root(gloo_utils::document().get_element_by_id("output").unwrap())
.hydrate();
sleep(Duration::from_millis(100)).await;
let result = obtain_result_by_id("output");
// no placeholders, hydration is successful and state 12345 is preserved.
assert_eq!(result, r#"<div><div>12345</div></div>"#);
}
#[wasm_bindgen_test]
async fn use_prepared_state_with_suspension_works() {
#[function_component]
fn Comp() -> HtmlResult {
let ctr = use_prepared_state!(async |_| -> u32 { 12345 }, ())?.unwrap_or_default();
Ok(html! {
<div>
{*ctr}
</div>
})
}
#[function_component]
fn App() -> Html {
html! {
<Suspense fallback={Html::default()}>
<div>
<Comp />
</div>
</Suspense>
}
}
let s = ServerRenderer::<App>::new().render().await;
assert_eq!(
s,
r#"<!--<[use_prepared_state::use_prepared_state_with_suspension_works::{{closure}}::App]>--><!--<[yew::suspense::component::feat_csr_ssr::Suspense]>--><!--<[yew::suspense::component::feat_csr_ssr::BaseSuspense]>--><!--<?>--><div><!--<[use_prepared_state::use_prepared_state_with_suspension_works::{{closure}}::Comp]>--><div>12345</div><script type="application/x-yew-comp-state">ATkwAAAB</script><!--</[use_prepared_state::use_prepared_state_with_suspension_works::{{closure}}::Comp]>--></div><!--</?>--><!--</[yew::suspense::component::feat_csr_ssr::BaseSuspense]>--><!--</[yew::suspense::component::feat_csr_ssr::Suspense]>--><!--</[use_prepared_state::use_prepared_state_with_suspension_works::{{closure}}::App]>-->"#
);
gloo::utils::document()
.query_selector("#output")
.unwrap()
.unwrap()
.set_inner_html(&s);
sleep(Duration::ZERO).await;
Renderer::<App>::with_root(gloo_utils::document().get_element_by_id("output").unwrap())
.hydrate();
sleep(Duration::from_millis(100)).await;
let result = obtain_result_by_id("output");
// no placeholders, hydration is successful and state 12345 is preserved.
assert_eq!(result, r#"<div><div>12345</div></div>"#);
}

View File

@ -0,0 +1,65 @@
#![cfg(feature = "hydration")]
#![cfg(target_arch = "wasm32")]
use std::time::Duration;
mod common;
use common::obtain_result_by_id;
use gloo::timers::future::sleep;
use wasm_bindgen_test::*;
use yew::prelude::*;
use yew::{Renderer, ServerRenderer};
wasm_bindgen_test::wasm_bindgen_test_configure!(run_in_browser);
#[wasm_bindgen_test]
async fn use_transitive_state_works() {
#[function_component]
fn Comp() -> HtmlResult {
let ctr = use_transitive_state!(|_| -> u32 { 12345 }, ())?.unwrap_or_default();
Ok(html! {
<div>
{*ctr}
</div>
})
}
#[function_component]
fn App() -> Html {
html! {
<Suspense fallback={Html::default()}>
<div>
<Comp />
</div>
</Suspense>
}
}
let s = ServerRenderer::<App>::new().render().await;
assert_eq!(
s,
// div text content should be 0 but state should be 12345.
r#"<!--<[use_transitive_state::use_transitive_state_works::{{closure}}::App]>--><!--<[yew::suspense::component::feat_csr_ssr::Suspense]>--><!--<[yew::suspense::component::feat_csr_ssr::BaseSuspense]>--><!--<?>--><div><!--<[use_transitive_state::use_transitive_state_works::{{closure}}::Comp]>--><div>0</div><script type="application/x-yew-comp-state">ATkwAAAB</script><!--</[use_transitive_state::use_transitive_state_works::{{closure}}::Comp]>--></div><!--</?>--><!--</[yew::suspense::component::feat_csr_ssr::BaseSuspense]>--><!--</[yew::suspense::component::feat_csr_ssr::Suspense]>--><!--</[use_transitive_state::use_transitive_state_works::{{closure}}::App]>-->"#
);
gloo::utils::document()
.query_selector("#output")
.unwrap()
.unwrap()
.set_inner_html(&s);
sleep(Duration::ZERO).await;
Renderer::<App>::with_root(gloo_utils::document().get_element_by_id("output").unwrap())
.hydrate();
sleep(Duration::from_millis(100)).await;
let result = obtain_result_by_id("output");
// no placeholders, hydration is successful and div text content now becomes 12345.
assert_eq!(result, r#"<div><div>12345</div></div>"#);
}