mirror of
https://github.com/yewstack/yew.git
synced 2025-12-08 21:26:25 +00:00
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:
parent
027ab6af8b
commit
b29b4535b7
9
.github/workflows/main-checks.yml
vendored
9
.github/workflows/main-checks.yml
vendored
@ -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
|
||||
|
||||
@ -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"]
|
||||
|
||||
@ -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
|
||||
|
||||
|
||||
@ -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>
|
||||
|
||||
@ -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>
|
||||
|
||||
@ -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"]
|
||||
|
||||
@ -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
|
||||
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -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()
|
||||
}
|
||||
|
||||
116
packages/yew-macro/src/use_prepared_state.rs
Normal file
116
packages/yew-macro/src/use_prepared_state.rs
Normal 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)
|
||||
}
|
||||
}
|
||||
}
|
||||
82
packages/yew-macro/src/use_transitive_state.rs
Normal file
82
packages/yew-macro/src/use_transitive_state.rs
Normal 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)
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -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() {}
|
||||
@ -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)
|
||||
@ -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() {}
|
||||
@ -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)
|
||||
7
packages/yew-macro/tests/hook_macro_test.rs
Normal file
7
packages/yew-macro/tests/hook_macro_test.rs
Normal 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");
|
||||
}
|
||||
@ -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]
|
||||
|
||||
@ -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();
|
||||
}
|
||||
|
||||
@ -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;
|
||||
|
||||
|
||||
@ -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,
|
||||
}
|
||||
}
|
||||
@ -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 }
|
||||
}
|
||||
@ -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)
|
||||
}
|
||||
143
packages/yew/src/functional/hooks/use_prepared_state/feat_ssr.rs
Normal file
143
packages/yew/src/functional/hooks/use_prepared_state/feat_ssr.rs
Normal 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 }
|
||||
}
|
||||
146
packages/yew/src/functional/hooks/use_prepared_state/mod.rs
Normal file
146
packages/yew/src/functional/hooks/use_prepared_state/mod.rs
Normal 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;
|
||||
@ -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;
|
||||
@ -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 }
|
||||
}
|
||||
@ -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;
|
||||
@ -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 }
|
||||
}
|
||||
@ -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;
|
||||
@ -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>
|
||||
|
||||
@ -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,
|
||||
));
|
||||
}
|
||||
}
|
||||
|
||||
@ -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)
|
||||
}
|
||||
}
|
||||
|
||||
@ -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(),
|
||||
|
||||
114
packages/yew/tests/use_prepared_state.rs
Normal file
114
packages/yew/tests/use_prepared_state.rs
Normal 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>"#);
|
||||
}
|
||||
65
packages/yew/tests/use_transitive_state.rs
Normal file
65
packages/yew/tests/use_transitive_state.rs
Normal 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>"#);
|
||||
}
|
||||
Loading…
x
Reference in New Issue
Block a user