From 7aa57b05ddf6303f2ae309b3ff4fb7437bb85043 Mon Sep 17 00:00:00 2001 From: Kaede Hoshikawa Date: Mon, 18 Apr 2022 22:11:40 +0900 Subject: [PATCH 01/31] Some initial implementation. --- packages/yew-macro/src/function_component.rs | 5 + packages/yew-macro/src/lib.rs | 14 ++ packages/yew-macro/src/use_prepared_state.rs | 110 +++++++++++++ packages/yew/Cargo.toml | 7 +- packages/yew/src/functional/hooks/mod.rs | 2 + .../use_prepared_state/feat_hydration.rs | 67 ++++++++ .../use_prepared_state/feat_hydration_ssr.rs | 27 +++ .../hooks/use_prepared_state/feat_none.rs | 29 ++++ .../hooks/use_prepared_state/feat_ssr.rs | 155 ++++++++++++++++++ .../hooks/use_prepared_state/mod.rs | 57 +++++++ packages/yew/src/functional/mod.rs | 70 +++++++- packages/yew/src/html/component/mod.rs | 10 ++ packages/yew/src/html/component/scope.rs | 8 + 13 files changed, 555 insertions(+), 6 deletions(-) create mode 100644 packages/yew-macro/src/use_prepared_state.rs create mode 100644 packages/yew/src/functional/hooks/use_prepared_state/feat_hydration.rs create mode 100644 packages/yew/src/functional/hooks/use_prepared_state/feat_hydration_ssr.rs create mode 100644 packages/yew/src/functional/hooks/use_prepared_state/feat_none.rs create mode 100644 packages/yew/src/functional/hooks/use_prepared_state/feat_ssr.rs create mode 100644 packages/yew/src/functional/hooks/use_prepared_state/mod.rs diff --git a/packages/yew-macro/src/function_component.rs b/packages/yew-macro/src/function_component.rs index f5ad0008e03..e98a8328b51 100644 --- a/packages/yew-macro/src/function_component.rs +++ b/packages/yew-macro/src/function_component.rs @@ -319,6 +319,11 @@ impl FunctionComponent { fn destroy(&mut self, _ctx: &::yew::html::Context) { ::yew::functional::FunctionComponent::::destroy(&self.function_component) } + + #[inline] + fn prepare_state(&self) -> ::std::option::Option<::std::pin::Pin<::std::boxed::Box>>>> { + ::yew::functional::FunctionComponent::::prepare_state(&self.function_component) + } } } } diff --git a/packages/yew-macro/src/lib.rs b/packages/yew-macro/src/lib.rs index b01a9595c44..52561c48586 100644 --- a/packages/yew-macro/src/lib.rs +++ b/packages/yew-macro/src/lib.rs @@ -53,6 +53,7 @@ mod hook; mod html_tree; mod props; mod stringify; +mod use_prepared_state; use derive_props::DerivePropsInput; use function_component::{function_component_impl, FunctionComponent, FunctionComponentName}; @@ -62,6 +63,7 @@ use proc_macro::TokenStream; use quote::ToTokens; use syn::buffer::Cursor; use syn::parse_macro_input; +use use_prepared_state::PreparedState; trait Peek<'a, T> { fn peek(cursor: Cursor<'a>) -> Option<(T, Cursor<'a>)>; @@ -150,3 +152,15 @@ 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() +} diff --git a/packages/yew-macro/src/use_prepared_state.rs b/packages/yew-macro/src/use_prepared_state.rs new file mode 100644 index 00000000000..7b8a0743fd3 --- /dev/null +++ b/packages/yew-macro/src/use_prepared_state.rs @@ -0,0 +1,110 @@ +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 { + // Reads a closure. + let closure: ExprClosure = input.parse()?; + + input.parse::()?; + + 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 + } + + 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; + + match &self.closure.asyncness { + Some(_) => quote! { + ::yew::functional::use_prepared_state_with_suspension::<#rt, _>(#deps) + }, + None => quote! { + ::yew::functional::use_prepared_state::<#rt, _>(#deps) + }, + } + } +} diff --git a/packages/yew/Cargo.toml b/packages/yew/Cargo.toml index dddf528acbc..818fbcf7cd8 100644 --- a/packages/yew/Cargo.toml +++ b/packages/yew/Cargo.toml @@ -29,6 +29,9 @@ thiserror = "1.0" futures = { version = "0.3", optional = true } html-escape = { version = "0.2.9", optional = true } +base64 = { version = "0.13.0", optional = true } +bincode = "1.3.3" +serde = { version = "1", features = ["derive"] } [dependencies.web-sys] version = "0.3" @@ -86,9 +89,9 @@ features = [ ] [features] -ssr = ["futures", "html-escape"] +ssr = ["futures", "html-escape", "base64"] csr = [] -hydration = ["csr"] +hydration = ["csr", "base64"] trace_hydration = ["hydration"] doc_test = ["csr", "hydration", "ssr"] wasm_test = ["csr", "hydration", "ssr"] diff --git a/packages/yew/src/functional/hooks/mod.rs b/packages/yew/src/functional/hooks/mod.rs index 57470f2aa03..ba5c2bc584c 100644 --- a/packages/yew/src/functional/hooks/mod.rs +++ b/packages/yew/src/functional/hooks/mod.rs @@ -3,6 +3,7 @@ mod use_context; mod use_effect; mod use_force_update; mod use_memo; +mod use_prepared_state; mod use_reducer; mod use_ref; mod use_state; @@ -12,6 +13,7 @@ pub use use_context::*; pub use use_effect::*; pub use use_force_update::*; pub use use_memo::*; +pub use use_prepared_state::*; pub use use_reducer::*; pub use use_ref::*; pub use use_state::*; diff --git a/packages/yew/src/functional/hooks/use_prepared_state/feat_hydration.rs b/packages/yew/src/functional/hooks/use_prepared_state/feat_hydration.rs new file mode 100644 index 00000000000..d962fdb397e --- /dev/null +++ b/packages/yew/src/functional/hooks/use_prepared_state/feat_hydration.rs @@ -0,0 +1,67 @@ +use crate::functional::{Hook, HookContext}; +use crate::hook; + +use std::marker::PhantomData; +use std::rc::Rc; + +use super::PreparedStateBase; +use crate::suspense::SuspensionResult; +use serde::de::DeserializeOwned; +use serde::Serialize; + +/// The client-side rendering variant. This is used for client side rendering. +#[doc(hidden)] +pub fn use_prepared_state(deps: D) -> impl Hook>> +where + D: Serialize + DeserializeOwned + PartialEq + 'static, + T: Serialize + DeserializeOwned + 'static, +{ + struct HookProvider + where + D: Serialize + DeserializeOwned + PartialEq + 'static, + T: Serialize + DeserializeOwned + 'static, + { + _marker: PhantomData<(T, D)>, + deps: D, + } + + impl Hook for HookProvider + where + D: Serialize + DeserializeOwned + PartialEq + 'static, + T: Serialize + DeserializeOwned + 'static, + { + type Output = Option>; + + fn run(self, ctx: &mut HookContext) -> Self::Output { + let state = ctx.next_prepared_state(|_re_render, buf| -> PreparedStateBase { + buf.map(|buf| PreparedStateBase::::decode(buf)) + .unwrap_or(PreparedStateBase { + state: None, + deps: None, + }) + }); + + if state.deps.as_deref() == Some(&self.deps) { + return state.state.clone(); + } + + None + } + } + + HookProvider:: { + _marker: PhantomData, + deps, + } +} + +/// The with suspension variant for use_prepared_state_on_client_side. +#[doc(hidden)] +#[hook] +pub fn use_prepared_state_with_suspension(deps: D) -> SuspensionResult>> +where + D: Serialize + DeserializeOwned + PartialEq + 'static, + T: Serialize + DeserializeOwned + 'static, +{ + Ok(use_prepared_state(deps)) +} diff --git a/packages/yew/src/functional/hooks/use_prepared_state/feat_hydration_ssr.rs b/packages/yew/src/functional/hooks/use_prepared_state/feat_hydration_ssr.rs new file mode 100644 index 00000000000..38c76a47c40 --- /dev/null +++ b/packages/yew/src/functional/hooks/use_prepared_state/feat_hydration_ssr.rs @@ -0,0 +1,27 @@ +use crate::hook; + +use std::rc::Rc; + +use crate::suspense::SuspensionResult; +use serde::de::DeserializeOwned; +use serde::Serialize; + +#[hook] +pub fn use_prepared_state(f: F, deps: D) -> Option> +where + D: Serialize + DeserializeOwned + PartialEq + 'static, + T: Serialize + DeserializeOwned + 'static, + F: FnOnce(&D) -> T, +{ + todo!() +} + +#[hook] +pub fn use_prepared_state_with_suspension(f: F, deps: D) -> SuspensionResult>> +where + D: Serialize + DeserializeOwned + PartialEq + 'static, + T: Serialize + DeserializeOwned + 'static, + F: FnOnce(&D) -> T, +{ + todo!() +} diff --git a/packages/yew/src/functional/hooks/use_prepared_state/feat_none.rs b/packages/yew/src/functional/hooks/use_prepared_state/feat_none.rs new file mode 100644 index 00000000000..11554263db9 --- /dev/null +++ b/packages/yew/src/functional/hooks/use_prepared_state/feat_none.rs @@ -0,0 +1,29 @@ +use crate::hook; + +use std::rc::Rc; + +use crate::suspense::SuspensionResult; +use serde::de::DeserializeOwned; +use serde::Serialize; + +/// The noop variant. This is used when its client side rendering and hydration is not enabled. +#[doc(hidden)] +#[hook] +pub fn use_prepared_state(_deps: D) -> Option> +where + D: Serialize + DeserializeOwned + PartialEq + 'static, + T: Serialize + DeserializeOwned + 'static, +{ + None +} + +/// The with suspension variant for use_prepared_state_with_noop. +#[doc(hidden)] +#[hook] +pub fn use_prepared_state_with_suspension(_deps: D) -> SuspensionResult>> +where + D: Serialize + DeserializeOwned + PartialEq + 'static, + T: Serialize + DeserializeOwned + 'static, +{ + Ok(None) +} diff --git a/packages/yew/src/functional/hooks/use_prepared_state/feat_ssr.rs b/packages/yew/src/functional/hooks/use_prepared_state/feat_ssr.rs new file mode 100644 index 00000000000..071a09c1e05 --- /dev/null +++ b/packages/yew/src/functional/hooks/use_prepared_state/feat_ssr.rs @@ -0,0 +1,155 @@ +use super::{use_memo, Hook, HookContext}; + +use std::marker::PhantomData; +use std::rc::Rc; + +use super::PreparedStateBase; +use serde::de::DeserializeOwned; +use serde::Serialize; + +/// The server-side rendering variant. This is used for server side rendering. +#[doc(hidden)] +pub fn use_prepared_state(f: F, deps: D) -> impl Hook>> +where + D: Serialize + DeserializeOwned + PartialEq + 'static, + T: Serialize + DeserializeOwned + 'static, + F: FnOnce(&D) -> T, +{ + struct HookProvider + where + D: Serialize + DeserializeOwned + PartialEq + 'static, + T: Serialize + DeserializeOwned + 'static, + F: FnOnce(&D) -> T, + { + _marker: PhantomData<(T, D)>, + deps: D, + f: F, + } + + impl Hook for HookProvider + where + D: Serialize + DeserializeOwned + PartialEq + 'static, + T: Serialize + DeserializeOwned + 'static, + F: FnOnce(&D) -> T, + { + type Output = Option>; + + 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), + }; + + let state = + ctx.next_prepared_state(|_re_render, _| -> PreparedStateBase { state }); + + state.state.clone() + } + } + + HookProvider:: { + _marker: PhantomData, + deps, + f, + } +} + +#[cfg_attr(documenting, doc(cfg(any(target_arch = "wasm32", feature = "tokio"))))] +#[cfg(any(target_arch = "wasm32", feature = "tokio"))] +mod feat_io { + use super::*; + + use std::future::Future; + + use crate::functional::use_state; + use crate::io_coop::spawn_local; + use crate::suspense::{Suspension, SuspensionResult}; + + /// The with suspension variant for use_prepared_state_on_server_side. + #[doc(hidden)] + pub fn use_prepared_state_with_suspension( + f: F, + deps: D, + ) -> impl Hook>>> + where + D: Serialize + DeserializeOwned + PartialEq + 'static, + T: Serialize + DeserializeOwned + 'static, + F: FnOnce(&D) -> U, + U: 'static + Future, + { + struct HookProvider + where + D: Serialize + DeserializeOwned + PartialEq + 'static, + T: Serialize + DeserializeOwned + 'static, + F: FnOnce(&D) -> U, + U: 'static + Future, + { + _marker: PhantomData<(T, D)>, + deps: D, + f: F, + } + + impl Hook for HookProvider + where + D: Serialize + DeserializeOwned + PartialEq + 'static, + T: Serialize + DeserializeOwned + 'static, + F: FnOnce(&D) -> U, + U: 'static + Future, + { + type Output = SuspensionResult>>; + + 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)); + }) + }); + } + + let state = result.0.clone()?; + + let state = PreparedStateBase { + state: Some(state.into()), + deps: Some(deps), + }; + + let state = + ctx.next_prepared_state(|_re_render, _| -> PreparedStateBase { state }); + + Ok(state.state.clone()) + } + } + + HookProvider:: { + _marker: PhantomData, + deps, + f, + } + } +} + +#[cfg_attr(documenting, doc(cfg(any(target_arch = "wasm32", feature = "tokio"))))] +#[cfg(any(target_arch = "wasm32", feature = "tokio"))] +pub use feat_io::*; diff --git a/packages/yew/src/functional/hooks/use_prepared_state/mod.rs b/packages/yew/src/functional/hooks/use_prepared_state/mod.rs new file mode 100644 index 00000000000..7f670871cfa --- /dev/null +++ b/packages/yew/src/functional/hooks/use_prepared_state/mod.rs @@ -0,0 +1,57 @@ +use crate::functional::PreparedState; + +#[cfg(feature = "ssr")] +use std::future::Future; +#[cfg(feature = "ssr")] +use std::pin::Pin; +use std::rc::Rc; + +use serde::de::DeserializeOwned; +use serde::Serialize; + +#[cfg(feature = "hydration")] +mod feat_hydration; +mod feat_hydration_ssr; +mod feat_none; +mod feat_ssr; + +struct PreparedStateBase +where + D: Serialize + DeserializeOwned + PartialEq + 'static, + T: Serialize + DeserializeOwned + 'static, +{ + state: Option>, + #[allow(dead_code)] + deps: Option>, +} + +#[cfg(feature = "hydration")] +impl PreparedStateBase +where + D: Serialize + DeserializeOwned + PartialEq + 'static, + T: Serialize + DeserializeOwned + 'static, +{ + fn decode(buf: &[u8]) -> Self { + let (state, deps) = + bincode::deserialize::<(T, D)>(buf).expect("failed to deserialize state"); + + PreparedStateBase { + state: Some(Rc::new(state)), + deps: Some(Rc::new(deps)), + } + } +} + +impl PreparedState for PreparedStateBase +where + D: Serialize + DeserializeOwned + PartialEq + 'static, + T: Serialize + DeserializeOwned + 'static, +{ + #[cfg(feature = "ssr")] + fn prepare(&self) -> Pin>>> { + let state = bincode::serialize(&(self.state.as_deref(), self.deps.as_deref())) + .expect("failed to prepare state"); + + Box::pin(async move { state }) + } +} diff --git a/packages/yew/src/functional/mod.rs b/packages/yew/src/functional/mod.rs index ca4dc006c52..cdd52bbda02 100644 --- a/packages/yew/src/functional/mod.rs +++ b/packages/yew/src/functional/mod.rs @@ -19,13 +19,16 @@ //! //! More details about function components and Hooks can be found on [Yew Docs](https://yew.rs/docs/next/concepts/function-components/introduction) -use crate::html::{AnyScope, BaseComponent, Context, HtmlResult}; -use crate::Properties; use std::any::Any; use std::cell::RefCell; use std::fmt; +use std::future::Future; +use std::pin::Pin; use std::rc::Rc; +use crate::html::{AnyScope, BaseComponent, Context, HtmlResult}; +use crate::Properties; + use wasm_bindgen::prelude::*; mod hooks; @@ -64,7 +67,13 @@ pub use yew_macro::hook; type ReRender = Rc; -/// Primitives of a Hook state. +/// Primitives of a prepared state hook. +pub(crate) trait PreparedState { + #[cfg(feature = "ssr")] + fn prepare(&self) -> Pin>>>; +} + +/// Primitives of an effect hook. pub(crate) trait Effect { fn rendered(&self) {} } @@ -76,6 +85,7 @@ pub struct HookContext { states: Vec>, effects: Vec>, + prepared_states: Vec>, counter: usize, #[cfg(debug_assertions)] @@ -85,10 +95,12 @@ pub struct HookContext { impl HookContext { fn new(scope: AnyScope, re_render: ReRender) -> RefCell { RefCell::new(HookContext { - effects: Vec::new(), scope, re_render, + states: Vec::new(), + prepared_states: Vec::new(), + effects: Vec::new(), counter: 0, #[cfg(debug_assertions)] @@ -132,6 +144,24 @@ impl HookContext { t } + pub(crate) fn next_prepared_state( + &mut self, + initializer: impl FnOnce(ReRender, Option<&[u8]>) -> T, + ) -> Rc + where + T: 'static + PreparedState, + { + let prev_state_len = self.states.len(); + let t = self.next_state(move |re_render| initializer(re_render, None)); + + // 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) { self.counter = 0; @@ -184,6 +214,32 @@ impl HookContext { drop(state); } } + + #[cfg(not(feature = "ssr"))] + fn prepare_state(&self) -> Option>>>> { + None + } + + #[cfg(feature = "ssr")] + fn prepare_state(&self) -> Option>>>> { + if self.prepared_states.is_empty() { + return None; + } + + let prepared_states = self.prepared_states.clone(); + + Some(Box::pin(async move { + let mut states = Vec::with_capacity(prepared_states.len()); + + for state in prepared_states.iter() { + let state = state.prepare().await; + + states.push(state); + } + + bincode::serialize(&states).expect("failed to serialize state.") + })) + } } impl fmt::Debug for HookContext { @@ -268,6 +324,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>>>> { + let hook_ctx = self.hook_ctx.borrow(); + hook_ctx.prepare_state() + } } impl fmt::Debug for FunctionComponent diff --git a/packages/yew/src/html/component/mod.rs b/packages/yew/src/html/component/mod.rs index 07b81827a58..f776e857b52 100644 --- a/packages/yew/src/html/component/mod.rs +++ b/packages/yew/src/html/component/mod.rs @@ -1,5 +1,8 @@ //! Components wrapped with context including properties, state, and link +use std::future::Future; +use std::pin::Pin; + mod children; #[cfg(any(feature = "csr", feature = "ssr"))] mod lifecycle; @@ -119,6 +122,9 @@ pub trait BaseComponent: Sized + 'static { /// Notified before a component is destroyed. fn destroy(&mut self, ctx: &Context); + + /// Prepares the server-side state. + fn prepare_state(&self) -> Option>>>>; } /// Components are the basic building blocks of the UI in a Yew app. Each Component @@ -214,4 +220,8 @@ where fn destroy(&mut self, ctx: &Context) { Component::destroy(self, ctx) } + + fn prepare_state(&self) -> Option>>>> { + None + } } diff --git a/packages/yew/src/html/component/scope.rs b/packages/yew/src/html/component/scope.rs index b83a6a2d052..7fdf4122af6 100644 --- a/packages/yew/src/html/component/scope.rs +++ b/packages/yew/src/html/component/scope.rs @@ -231,6 +231,14 @@ mod feat_ssr { let self_any_scope = AnyScope::from(self.clone()); html.render_to_string(w, &self_any_scope, hydratable).await; + if let Some(prepare_state) = self.get_component().unwrap().prepare_state() { + let prepared_state = prepare_state.await; + + w.push_str(r#""#); } if hydratable { @@ -583,6 +583,7 @@ mod feat_hydration { { Some(m) if m.type_() == "application/x-yew-comp-state" => { fragment.pop_back(); + parent.remove_child(&m).unwrap(); Some(m.text().unwrap()) } _ => None, diff --git a/packages/yew/tests/use_prepared_state.rs b/packages/yew/tests/use_prepared_state.rs new file mode 100644 index 00000000000..597ea2571a0 --- /dev/null +++ b/packages/yew/tests/use_prepared_state.rs @@ -0,0 +1,111 @@ +#![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() -> Html { + let ctr = use_prepared_state!(|_| -> u32 { 12345 }, ()).unwrap_or_default(); + + html! { +
+ {*ctr} +
+ } + } + + #[function_component] + fn App() -> Html { + html! { +
+ +
+ } + } + + let s = ServerRenderer::::new().render().await; + + assert_eq!( + s, + r#"
12345
"# + ); + + gloo::utils::document() + .query_selector("#output") + .unwrap() + .unwrap() + .set_inner_html(&s); + + sleep(Duration::ZERO).await; + + Renderer::::with_root(gloo_utils::document().get_element_by_id("output").unwrap()) + .hydrate(); + + sleep(Duration::ZERO).await; + + let result = obtain_result_by_id("output"); + + // no placeholders, hydration is successful and state 12345 is preserved. + assert_eq!(result, r#"
12345
"#); +} + +#[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! { +
+ {*ctr} +
+ }) + } + + #[function_component] + fn App() -> Html { + html! { + +
+ +
+
+ } + } + + let s = ServerRenderer::::new().render().await; + + assert_eq!( + s, + r#"
12345
"# + ); + + gloo::utils::document() + .query_selector("#output") + .unwrap() + .unwrap() + .set_inner_html(&s); + + sleep(Duration::ZERO).await; + + Renderer::::with_root(gloo_utils::document().get_element_by_id("output").unwrap()) + .hydrate(); + + sleep(Duration::ZERO).await; + + let result = obtain_result_by_id("output"); + + // no placeholders, hydration is successful and state 12345 is preserved. + assert_eq!(result, r#"
12345
"#); +} diff --git a/rustfmt.toml b/rustfmt.toml index 473c88723dd..21571ed7ffa 100644 --- a/rustfmt.toml +++ b/rustfmt.toml @@ -10,3 +10,4 @@ group_imports = "StdExternalCrate" imports_granularity = "Module" reorder_impl_items = true use_field_init_shorthand = true +edition = "2021" From 95bb84df2f824222c9e23d732461bf628155c021 Mon Sep 17 00:00:00 2001 From: Kaede Hoshikawa Date: Sun, 24 Apr 2022 22:49:36 +0900 Subject: [PATCH 10/31] Minor adjustments. --- packages/yew/src/functional/hooks/use_prepared_state/feat_ssr.rs | 1 - packages/yew/src/html/component/lifecycle.rs | 1 - 2 files changed, 2 deletions(-) diff --git a/packages/yew/src/functional/hooks/use_prepared_state/feat_ssr.rs b/packages/yew/src/functional/hooks/use_prepared_state/feat_ssr.rs index 90a9a71c56b..1bd88493a7f 100644 --- a/packages/yew/src/functional/hooks/use_prepared_state/feat_ssr.rs +++ b/packages/yew/src/functional/hooks/use_prepared_state/feat_ssr.rs @@ -73,7 +73,6 @@ mod feat_io { use crate::io_coop::spawn_local; use crate::suspense::{Suspension, SuspensionResult}; - /// The with suspension variant for use_prepared_state_on_server_side. #[doc(hidden)] pub fn use_prepared_state_with_suspension( f: F, diff --git a/packages/yew/src/html/component/lifecycle.rs b/packages/yew/src/html/component/lifecycle.rs index ff2fcfe0fcc..325ec793f2a 100644 --- a/packages/yew/src/html/component/lifecycle.rs +++ b/packages/yew/src/html/component/lifecycle.rs @@ -462,7 +462,6 @@ impl RenderRunner { if suspension.resumed() { // schedule a render immediately if suspension is resumed. - scheduler::push_component_render( state.comp_id, Box::new(RenderRunner { From 5365ffdfdc104b695dd69d0fe0455195fc7c5193 Mon Sep 17 00:00:00 2001 From: Kaede Hoshikawa Date: Sun, 24 Apr 2022 23:07:04 +0900 Subject: [PATCH 11/31] Remove unused marker. --- .../hooks/use_prepared_state/feat_hydration.rs | 2 +- .../use_prepared_state/feat_hydration_ssr.rs | 17 +++-------------- .../hooks/use_prepared_state/feat_ssr.rs | 15 ++------------- 3 files changed, 6 insertions(+), 28 deletions(-) diff --git a/packages/yew/src/functional/hooks/use_prepared_state/feat_hydration.rs b/packages/yew/src/functional/hooks/use_prepared_state/feat_hydration.rs index f94d3664153..e5d0b4ac9d3 100644 --- a/packages/yew/src/functional/hooks/use_prepared_state/feat_hydration.rs +++ b/packages/yew/src/functional/hooks/use_prepared_state/feat_hydration.rs @@ -23,7 +23,7 @@ where D: Serialize + DeserializeOwned + PartialEq + 'static, T: Serialize + DeserializeOwned + 'static, { - _marker: PhantomData<(T, D)>, + _marker: PhantomData, deps: D, } diff --git a/packages/yew/src/functional/hooks/use_prepared_state/feat_hydration_ssr.rs b/packages/yew/src/functional/hooks/use_prepared_state/feat_hydration_ssr.rs index 3fb9d34b639..e96ba8ea408 100644 --- a/packages/yew/src/functional/hooks/use_prepared_state/feat_hydration_ssr.rs +++ b/packages/yew/src/functional/hooks/use_prepared_state/feat_hydration_ssr.rs @@ -1,6 +1,5 @@ //! The client-and-server-side rendering variant. -use std::marker::PhantomData; use std::rc::Rc; use serde::de::DeserializeOwned; @@ -9,7 +8,6 @@ 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(f: F, deps: D) -> impl Hook>> @@ -24,7 +22,6 @@ where T: Serialize + DeserializeOwned + 'static, F: FnOnce(&D) -> T, { - _marker: PhantomData<(T, D)>, deps: D, f: F, } @@ -45,11 +42,7 @@ where } } - HookProvider:: { - _marker: PhantomData, - deps, - f, - } + HookProvider:: { deps, f } } #[cfg_attr(documenting, doc(cfg(any(target_arch = "wasm32", feature = "tokio"))))] @@ -58,6 +51,7 @@ mod feat_io { use std::future::Future; use super::*; + use crate::suspense::SuspensionResult; #[doc(hidden)] pub fn use_prepared_state_with_suspension( @@ -77,7 +71,6 @@ mod feat_io { F: FnOnce(&D) -> U, U: 'static + Future, { - _marker: PhantomData<(T, D)>, deps: D, f: F, } @@ -101,11 +94,7 @@ mod feat_io { } } - HookProvider:: { - _marker: PhantomData, - deps, - f, - } + HookProvider:: { deps, f } } } diff --git a/packages/yew/src/functional/hooks/use_prepared_state/feat_ssr.rs b/packages/yew/src/functional/hooks/use_prepared_state/feat_ssr.rs index 1bd88493a7f..ad139c0c3a3 100644 --- a/packages/yew/src/functional/hooks/use_prepared_state/feat_ssr.rs +++ b/packages/yew/src/functional/hooks/use_prepared_state/feat_ssr.rs @@ -1,6 +1,5 @@ //! The server-side rendering variant. This is used for server side rendering. -use std::marker::PhantomData; use std::rc::Rc; use serde::de::DeserializeOwned; @@ -22,7 +21,6 @@ where T: Serialize + DeserializeOwned + 'static, F: FnOnce(&D) -> T, { - _marker: PhantomData<(T, D)>, deps: D, f: F, } @@ -56,11 +54,7 @@ where } } - HookProvider:: { - _marker: PhantomData, - deps, - f, - } + HookProvider:: { deps, f } } #[cfg_attr(documenting, doc(cfg(any(target_arch = "wasm32", feature = "tokio"))))] @@ -91,7 +85,6 @@ mod feat_io { F: FnOnce(&D) -> U, U: 'static + Future, { - _marker: PhantomData<(T, D)>, deps: D, f: F, } @@ -141,11 +134,7 @@ mod feat_io { } } - HookProvider:: { - _marker: PhantomData, - deps, - f, - } + HookProvider:: { deps, f } } } From 8c659db7f1792eb19672430c1acdc0ad8d2623aa Mon Sep 17 00:00:00 2001 From: Kaede Hoshikawa Date: Sat, 30 Apr 2022 23:47:42 +0900 Subject: [PATCH 12/31] Update example. --- examples/simple_ssr/Cargo.toml | 2 +- examples/simple_ssr/src/lib.rs | 57 +------------------ .../hooks/use_prepared_state/feat_ssr.rs | 4 +- 3 files changed, 5 insertions(+), 58 deletions(-) diff --git a/examples/simple_ssr/Cargo.toml b/examples/simple_ssr/Cargo.toml index a9559e37335..c59ff92ce80 100644 --- a/examples/simple_ssr/Cargo.toml +++ b/examples/simple_ssr/Cargo.toml @@ -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", features = ["ssr", "hydration", "tokio"] } reqwest = { version = "0.11.8", features = ["json"] } serde = { version = "1.0.132", features = ["derive"] } uuid = { version = "1.0.0", features = ["serde"] } diff --git a/examples/simple_ssr/src/lib.rs b/examples/simple_ssr/src/lib.rs index 893c2c94ce5..f61e3f64009 100644 --- a/examples/simple_ssr/src/lib.rs +++ b/examples/simple_ssr/src/lib.rs @@ -1,14 +1,6 @@ -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 { @@ -23,56 +15,9 @@ async fn fetch_uuid() -> Uuid { uuid_resp.uuid } -pub struct UuidState { - s: Suspension, - value: Rc>>, -} - -impl UuidState { - fn new() -> Self { - let (s, handle) = Suspension::new(); - let value: Rc>> = 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 { - 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! {
{"Random UUID: "}{uuid}
diff --git a/packages/yew/src/functional/hooks/use_prepared_state/feat_ssr.rs b/packages/yew/src/functional/hooks/use_prepared_state/feat_ssr.rs index ad139c0c3a3..26923d3295e 100644 --- a/packages/yew/src/functional/hooks/use_prepared_state/feat_ssr.rs +++ b/packages/yew/src/functional/hooks/use_prepared_state/feat_ssr.rs @@ -113,11 +113,13 @@ mod feat_io { 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()?; From 3657129f7be3240e0138f471b23367f7a77cb6da Mon Sep 17 00:00:00 2001 From: Kaede Hoshikawa Date: Sun, 1 May 2022 00:55:54 +0900 Subject: [PATCH 13/31] Add use_transitive_state. --- packages/yew-macro/src/function_component.rs | 2 +- packages/yew-macro/src/lib.rs | 14 ++++ .../yew-macro/src/use_transitive_state.rs | 72 +++++++++++++++++ packages/yew/src/functional/hooks/mod.rs | 2 + .../hooks/use_prepared_state/mod.rs | 20 +++-- .../use_transitive_state/feat_hydration.rs | 6 ++ .../feat_hydration_ssr.rs | 46 +++++++++++ .../hooks/use_transitive_state/feat_none.rs | 4 + .../hooks/use_transitive_state/feat_ssr.rs | 77 +++++++++++++++++++ .../hooks/use_transitive_state/mod.rs | 61 +++++++++++++++ packages/yew/src/functional/mod.rs | 41 ++++------ packages/yew/src/html/component/mod.rs | 8 +- packages/yew/src/html/component/scope.rs | 4 +- packages/yew/tests/use_prepared_state.rs | 2 +- 14 files changed, 310 insertions(+), 49 deletions(-) create mode 100644 packages/yew-macro/src/use_transitive_state.rs create mode 100644 packages/yew/src/functional/hooks/use_transitive_state/feat_hydration.rs create mode 100644 packages/yew/src/functional/hooks/use_transitive_state/feat_hydration_ssr.rs create mode 100644 packages/yew/src/functional/hooks/use_transitive_state/feat_none.rs create mode 100644 packages/yew/src/functional/hooks/use_transitive_state/feat_ssr.rs create mode 100644 packages/yew/src/functional/hooks/use_transitive_state/mod.rs diff --git a/packages/yew-macro/src/function_component.rs b/packages/yew-macro/src/function_component.rs index 3ae123b779d..2c36fe4c465 100644 --- a/packages/yew-macro/src/function_component.rs +++ b/packages/yew-macro/src/function_component.rs @@ -321,7 +321,7 @@ impl FunctionComponent { } #[inline] - fn prepare_state(&self) -> ::std::option::Option<::std::pin::Pin<::std::boxed::Box>>> { + fn prepare_state(&self) -> ::std::option::Option<::std::string::String> { ::yew::functional::FunctionComponent::::prepare_state(&self.function_component) } } diff --git a/packages/yew-macro/src/lib.rs b/packages/yew-macro/src/lib.rs index eba23782f2f..44966eabad0 100644 --- a/packages/yew-macro/src/lib.rs +++ b/packages/yew-macro/src/lib.rs @@ -54,6 +54,7 @@ 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}; @@ -64,6 +65,7 @@ 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>)>; @@ -170,3 +172,15 @@ 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() +} diff --git a/packages/yew-macro/src/use_transitive_state.rs b/packages/yew-macro/src/use_transitive_state.rs new file mode 100644 index 00000000000..bdcca330213 --- /dev/null +++ b/packages/yew-macro/src/use_transitive_state.rs @@ -0,0 +1,72 @@ +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 { + // Reads a closure. + let closure: ExprClosure = input.parse()?; + + input.parse::()?; + + 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) + } + } +} diff --git a/packages/yew/src/functional/hooks/mod.rs b/packages/yew/src/functional/hooks/mod.rs index 1f768b6d1bf..3fd8c70cde0 100644 --- a/packages/yew/src/functional/hooks/mod.rs +++ b/packages/yew/src/functional/hooks/mod.rs @@ -7,6 +7,7 @@ mod use_prepared_state; mod use_reducer; mod use_ref; mod use_state; +mod use_transitive_state; pub use use_callback::*; pub use use_context::*; @@ -17,6 +18,7 @@ pub use use_prepared_state::*; pub use use_reducer::*; pub use use_ref::*; pub use use_state::*; +pub use use_transitive_state::*; use crate::functional::{AnyScope, HookContext}; diff --git a/packages/yew/src/functional/hooks/use_prepared_state/mod.rs b/packages/yew/src/functional/hooks/use_prepared_state/mod.rs index 67081461e4c..25e78331778 100644 --- a/packages/yew/src/functional/hooks/use_prepared_state/mod.rs +++ b/packages/yew/src/functional/hooks/use_prepared_state/mod.rs @@ -1,9 +1,9 @@ #[cfg(feature = "hydration")] -mod feat_hydration; +pub(super) mod feat_hydration; #[cfg(all(feature = "hydration", feature = "ssr"))] mod feat_hydration_ssr; #[cfg(not(any(feature = "hydration", feature = "ssr")))] -mod feat_none; +pub(super) mod feat_none; #[cfg(feature = "ssr")] mod feat_ssr; @@ -86,15 +86,11 @@ pub use yew_macro::use_prepared_state_with_closure as use_prepared_state_macro; pub use yew_macro::use_prepared_state_with_closure_and_suspension as use_prepared_state_macro; // Without SSR. #[doc(hidden)] -#[cfg(not(feature = "ssr",))] +#[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 { - #[cfg(feature = "ssr")] - use std::future::Future; - #[cfg(feature = "ssr")] - use std::pin::Pin; use std::rc::Rc; use serde::de::DeserializeOwned; @@ -118,8 +114,10 @@ mod feat_any_hydration_ssr { D: Serialize + DeserializeOwned + PartialEq + 'static, T: Serialize + DeserializeOwned + 'static, { - pub fn decode(buf: &[u8]) -> Self { - let (state, deps) = bincode::deserialize::<(Option, Option)>(buf) + pub fn decode(s: &str) -> Self { + let buf = base64::decode(s).unwrap(); + + let (state, deps) = bincode::deserialize::<(Option, Option)>(&buf) .expect("failed to deserialize state"); PreparedStateBase { @@ -135,11 +133,11 @@ mod feat_any_hydration_ssr { T: Serialize + DeserializeOwned + 'static, { #[cfg(feature = "ssr")] - fn prepare(&self) -> Pin>>> { + fn prepare(&self) -> String { let state = bincode::serialize(&(self.state.as_deref(), self.deps.as_deref())) .expect("failed to prepare state"); - Box::pin(async move { state }) + base64::encode(&state) } } } diff --git a/packages/yew/src/functional/hooks/use_transitive_state/feat_hydration.rs b/packages/yew/src/functional/hooks/use_transitive_state/feat_hydration.rs new file mode 100644 index 00000000000..b779ddceb3a --- /dev/null +++ b/packages/yew/src/functional/hooks/use_transitive_state/feat_hydration.rs @@ -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; diff --git a/packages/yew/src/functional/hooks/use_transitive_state/feat_hydration_ssr.rs b/packages/yew/src/functional/hooks/use_transitive_state/feat_hydration_ssr.rs new file mode 100644 index 00000000000..365eeb2d84e --- /dev/null +++ b/packages/yew/src/functional/hooks/use_transitive_state/feat_hydration_ssr.rs @@ -0,0 +1,46 @@ +//! 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; + +#[doc(hidden)] +pub fn use_transitive_state(f: F, deps: D) -> impl Hook>> +where + D: Serialize + DeserializeOwned + PartialEq + 'static, + T: Serialize + DeserializeOwned + 'static, + F: 'static + FnOnce(&D) -> T, +{ + struct HookProvider + where + D: Serialize + DeserializeOwned + PartialEq + 'static, + T: Serialize + DeserializeOwned + 'static, + F: 'static + FnOnce(&D) -> T, + { + deps: D, + f: F, + } + + impl Hook for HookProvider + where + D: Serialize + DeserializeOwned + PartialEq + 'static, + T: Serialize + DeserializeOwned + 'static, + F: 'static + FnOnce(&D) -> T, + { + type Output = Option>; + + 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:: { deps, f } +} diff --git a/packages/yew/src/functional/hooks/use_transitive_state/feat_none.rs b/packages/yew/src/functional/hooks/use_transitive_state/feat_none.rs new file mode 100644 index 00000000000..f5c4651b600 --- /dev/null +++ b/packages/yew/src/functional/hooks/use_transitive_state/feat_none.rs @@ -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; diff --git a/packages/yew/src/functional/hooks/use_transitive_state/feat_ssr.rs b/packages/yew/src/functional/hooks/use_transitive_state/feat_ssr.rs new file mode 100644 index 00000000000..49a01d522d9 --- /dev/null +++ b/packages/yew/src/functional/hooks/use_transitive_state/feat_ssr.rs @@ -0,0 +1,77 @@ +//! The server-side rendering variant. + +use std::cell::RefCell; +use std::rc::Rc; + +use serde::de::DeserializeOwned; +use serde::Serialize; + +use crate::functional::{Hook, HookContext, PreparedState}; + +pub(super) struct TransitiveStateBase +where + D: Serialize + DeserializeOwned + PartialEq + 'static, + T: Serialize + DeserializeOwned + 'static, + F: 'static + FnOnce(&D) -> T, +{ + pub state_fn: RefCell>, + pub deps: D, +} + +impl PreparedState for TransitiveStateBase +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(&(&state, &self.deps)).expect("failed to prepare state"); + + base64::encode(&state) + } +} + +#[doc(hidden)] +pub fn use_transitive_state(f: F, deps: D) -> impl Hook>> +where + D: Serialize + DeserializeOwned + PartialEq + 'static, + T: Serialize + DeserializeOwned + 'static, + F: 'static + FnOnce(&D) -> T, +{ + struct HookProvider + where + D: Serialize + DeserializeOwned + PartialEq + 'static, + T: Serialize + DeserializeOwned + 'static, + F: 'static + FnOnce(&D) -> T, + { + deps: D, + f: F, + } + + impl Hook for HookProvider + where + D: Serialize + DeserializeOwned + PartialEq + 'static, + T: Serialize + DeserializeOwned + 'static, + F: 'static + FnOnce(&D) -> T, + { + type Output = Option>; + + fn run(self, ctx: &mut HookContext) -> Self::Output { + let f = self.f; + + ctx.next_prepared_state(move |_re_render, _| -> TransitiveStateBase { + TransitiveStateBase { + state_fn: Some(f).into(), + deps: self.deps, + } + }); + + None + } + } + + HookProvider:: { deps, f } +} diff --git a/packages/yew/src/functional/hooks/use_transitive_state/mod.rs b/packages/yew/src/functional/hooks/use_transitive_state/mod.rs new file mode 100644 index 00000000000..a3791b1b924 --- /dev/null +++ b/packages/yew/src/functional/hooks/use_transitive_state/mod.rs @@ -0,0 +1,61 @@ +#[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 `Option>`. +/// +/// It will always return `None` during server-side rendering. +/// +/// During hydration, it will only return `Some(Rc)` if the component is hydrated from a +/// server-side rendering artifact and its dependency value matches. +/// +/// `let state = use_transitive_state!(|deps| -> ReturnType { ... }, deps);` +/// +/// ``` +/// # use yew::prelude::*; +/// # use serde::{Serialize, DeserializeOwned}; +/// # use std::rc::Rc; +/// #[hook] +/// pub fn use_transitive_state(f: F, deps: D) -> Option> +/// 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. +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; diff --git a/packages/yew/src/functional/mod.rs b/packages/yew/src/functional/mod.rs index b665bf25ceb..91c4ae1111e 100644 --- a/packages/yew/src/functional/mod.rs +++ b/packages/yew/src/functional/mod.rs @@ -23,8 +23,6 @@ use std::any::Any; use std::cell::RefCell; use std::fmt; -use std::future::Future; -use std::pin::Pin; use std::rc::Rc; use wasm_bindgen::prelude::*; @@ -72,7 +70,7 @@ type ReRender = Rc; #[cfg(any(feature = "hydration", feature = "ssr"))] pub(crate) trait PreparedState { #[cfg(feature = "ssr")] - fn prepare(&self) -> Pin>>>; + fn prepare(&self) -> String; } /// Primitives of an effect hook. @@ -94,7 +92,7 @@ pub struct HookContext { prepared_states: Vec>, #[cfg(feature = "hydration")] - prepared_states_data: Vec>, + prepared_states_data: Vec>, #[cfg(feature = "hydration")] prepared_state_counter: usize, @@ -126,11 +124,7 @@ impl HookContext { #[cfg(feature = "hydration")] prepared_states_data: { match prepared_state { - Some(m) => m - .split(',') - .map(|m| base64::decode_config(m.trim(), base64::STANDARD).unwrap()) - .map(Rc::from) - .collect(), + Some(m) => m.split(',').map(Rc::from).collect(), None => Vec::new(), } }, @@ -182,13 +176,13 @@ impl HookContext { #[cfg(any(feature = "hydration", feature = "ssr"))] pub(crate) fn next_prepared_state( &mut self, - initializer: impl FnOnce(ReRender, Option<&[u8]>) -> T, + initializer: impl FnOnce(ReRender, Option<&str>) -> T, ) -> Rc where T: 'static + PreparedState, { #[cfg(not(feature = "hydration"))] - let prepared_state = Option::>::None; + let prepared_state = Option::>::None; #[cfg(feature = "hydration")] let prepared_state = { @@ -268,33 +262,26 @@ impl HookContext { } #[cfg(not(feature = "ssr"))] - fn prepare_state(&self) -> Option>>> { + fn prepare_state(&self) -> Option { None } #[cfg(feature = "ssr")] - fn prepare_state(&self) -> Option>>> { + fn prepare_state(&self) -> Option { if self.prepared_states.is_empty() { return None; } let prepared_states = self.prepared_states.clone(); - Some(Box::pin(async move { - let mut states = "".to_string(); + let mut states = Vec::new(); - for state in prepared_states.iter() { - if !states.is_empty() { - states.push(','); - } - - let state = state.prepare().await; - - base64::encode_config_buf(&state, base64::STANDARD, &mut states); - } + for state in prepared_states.iter() { + let state = state.prepare(); + states.push(state); + } - states - })) + Some(states.join(",")) } } @@ -390,7 +377,7 @@ where } /// Prepares the server-side state. - pub fn prepare_state(&self) -> Option>>> { + pub fn prepare_state(&self) -> Option { let hook_ctx = self.hook_ctx.borrow(); hook_ctx.prepare_state() } diff --git a/packages/yew/src/html/component/mod.rs b/packages/yew/src/html/component/mod.rs index 6b2940de89a..dc2429cbd1a 100644 --- a/packages/yew/src/html/component/mod.rs +++ b/packages/yew/src/html/component/mod.rs @@ -1,8 +1,5 @@ //! Components wrapped with context including properties, state, and link -use std::future::Future; -use std::pin::Pin; - mod children; #[cfg(any(feature = "csr", feature = "ssr"))] mod lifecycle; @@ -133,7 +130,7 @@ pub trait BaseComponent: Sized + 'static { fn destroy(&mut self, ctx: &Context); /// Prepares the server-side state. - fn prepare_state(&self) -> Option>>>; + fn prepare_state(&self) -> Option; } /// Components are the basic building blocks of the UI in a Yew app. Each Component @@ -239,8 +236,7 @@ where Component::destroy(self, ctx) } - fn prepare_state(&self) -> Option>>> { + fn prepare_state(&self) -> Option { Component::prepare_state(self) - .map(|m| Box::pin(async move { m }) as Pin>>) } } diff --git a/packages/yew/src/html/component/scope.rs b/packages/yew/src/html/component/scope.rs index 08b87424177..39e75be0767 100644 --- a/packages/yew/src/html/component/scope.rs +++ b/packages/yew/src/html/component/scope.rs @@ -231,9 +231,7 @@ mod feat_ssr { let self_any_scope = AnyScope::from(self.clone()); html.render_to_string(w, &self_any_scope, hydratable).await; - if let Some(prepare_state) = self.get_component().unwrap().prepare_state() { - let prepared_state = prepare_state.await; - + if let Some(prepared_state) = self.get_component().unwrap().prepare_state() { w.push_str(r#""#); diff --git a/packages/yew/tests/use_prepared_state.rs b/packages/yew/tests/use_prepared_state.rs index 597ea2571a0..63670d7ad70 100644 --- a/packages/yew/tests/use_prepared_state.rs +++ b/packages/yew/tests/use_prepared_state.rs @@ -88,7 +88,7 @@ async fn use_prepared_state_with_suspension_works() { assert_eq!( s, - r#"
12345
"# + r#"
12345
"# ); gloo::utils::document() From b9aa42e3f1b740d43201a99a2c531abcb28e23c7 Mon Sep 17 00:00:00 2001 From: Kaede Hoshikawa Date: Sun, 1 May 2022 01:02:43 +0900 Subject: [PATCH 14/31] Remove unused dead code notation. --- packages/yew/src/functional/hooks/use_prepared_state/mod.rs | 1 - 1 file changed, 1 deletion(-) diff --git a/packages/yew/src/functional/hooks/use_prepared_state/mod.rs b/packages/yew/src/functional/hooks/use_prepared_state/mod.rs index 25e78331778..f23bfa6fb81 100644 --- a/packages/yew/src/functional/hooks/use_prepared_state/mod.rs +++ b/packages/yew/src/functional/hooks/use_prepared_state/mod.rs @@ -104,7 +104,6 @@ mod feat_any_hydration_ssr { T: Serialize + DeserializeOwned + 'static, { pub state: Option>, - #[allow(dead_code)] pub deps: Option>, } From 72253e513a281ad3ebfd28b41be75be4b58b7b92 Mon Sep 17 00:00:00 2001 From: Kaede Hoshikawa Date: Sun, 1 May 2022 01:35:08 +0900 Subject: [PATCH 15/31] Opt for better code size. --- packages/yew/Cargo.toml | 6 +++--- .../yew/src/functional/hooks/use_prepared_state/mod.rs | 5 +++-- .../functional/hooks/use_transitive_state/feat_ssr.rs | 3 ++- packages/yew/src/html/component/lifecycle.rs | 2 -- packages/yew/src/html/component/mod.rs | 9 ++++++++- 5 files changed, 16 insertions(+), 9 deletions(-) diff --git a/packages/yew/Cargo.toml b/packages/yew/Cargo.toml index 99ef3bcb5ea..72751dfd8b2 100644 --- a/packages/yew/Cargo.toml +++ b/packages/yew/Cargo.toml @@ -29,7 +29,7 @@ thiserror = "1.0" futures = { version = "0.3", optional = true } html-escape = { version = "0.2.9", optional = true } -base64 = { version = "0.13.0", optional = true } +base64ct = { version = "1.5.0", features = ["std"], optional = true } bincode = { version = "1.3.3", optional = true } serde = { version = "1", features = ["derive"] } @@ -91,9 +91,9 @@ features = [ ] [features] -ssr = ["futures", "html-escape", "base64", "bincode"] +ssr = ["futures", "html-escape", "base64ct", "bincode"] csr = [] -hydration = ["csr", "base64", "bincode"] +hydration = ["csr", "base64ct", "bincode"] trace_hydration = ["hydration"] doc_test = ["csr", "hydration", "ssr"] wasm_test = ["csr", "hydration", "ssr"] diff --git a/packages/yew/src/functional/hooks/use_prepared_state/mod.rs b/packages/yew/src/functional/hooks/use_prepared_state/mod.rs index f23bfa6fb81..819c2751ed1 100644 --- a/packages/yew/src/functional/hooks/use_prepared_state/mod.rs +++ b/packages/yew/src/functional/hooks/use_prepared_state/mod.rs @@ -93,6 +93,7 @@ pub use yew_macro::use_prepared_state_without_closure as use_prepared_state_macr mod feat_any_hydration_ssr { use std::rc::Rc; + use base64ct::{Base64, Encoding}; use serde::de::DeserializeOwned; use serde::Serialize; @@ -114,7 +115,7 @@ mod feat_any_hydration_ssr { T: Serialize + DeserializeOwned + 'static, { pub fn decode(s: &str) -> Self { - let buf = base64::decode(s).unwrap(); + let buf = Base64::decode_vec(s).unwrap(); let (state, deps) = bincode::deserialize::<(Option, Option)>(&buf) .expect("failed to deserialize state"); @@ -136,7 +137,7 @@ mod feat_any_hydration_ssr { let state = bincode::serialize(&(self.state.as_deref(), self.deps.as_deref())) .expect("failed to prepare state"); - base64::encode(&state) + Base64::encode_string(&state) } } } diff --git a/packages/yew/src/functional/hooks/use_transitive_state/feat_ssr.rs b/packages/yew/src/functional/hooks/use_transitive_state/feat_ssr.rs index 49a01d522d9..1b7751b9858 100644 --- a/packages/yew/src/functional/hooks/use_transitive_state/feat_ssr.rs +++ b/packages/yew/src/functional/hooks/use_transitive_state/feat_ssr.rs @@ -3,6 +3,7 @@ use std::cell::RefCell; use std::rc::Rc; +use base64ct::{Base64, Encoding}; use serde::de::DeserializeOwned; use serde::Serialize; @@ -30,7 +31,7 @@ where let state = bincode::serialize(&(&state, &self.deps)).expect("failed to prepare state"); - base64::encode(&state) + Base64::encode_string(&state) } } diff --git a/packages/yew/src/html/component/lifecycle.rs b/packages/yew/src/html/component/lifecycle.rs index 325ec793f2a..809b863bbb7 100644 --- a/packages/yew/src/html/component/lifecycle.rs +++ b/packages/yew/src/html/component/lifecycle.rs @@ -250,8 +250,6 @@ impl ComponentState { props, #[cfg(feature = "hydration")] mode, - #[cfg(not(feature = "hydration"))] - prepared_state: None, #[cfg(feature = "hydration")] prepared_state, }; diff --git a/packages/yew/src/html/component/mod.rs b/packages/yew/src/html/component/mod.rs index dc2429cbd1a..aaf588ad345 100644 --- a/packages/yew/src/html/component/mod.rs +++ b/packages/yew/src/html/component/mod.rs @@ -70,6 +70,7 @@ pub struct Context { #[cfg(feature = "hydration")] mode: RenderMode, + #[cfg(feature = "hydration")] prepared_state: Option, } @@ -93,7 +94,13 @@ impl Context { /// The component's prepared state pub fn prepared_state(&self) -> Option<&str> { - self.prepared_state.as_deref() + #[cfg(not(feature = "hydration"))] + let state = None; + + #[cfg(feature = "hydration")] + let state = self.prepared_state.as_deref(); + + state } } From 3faa58c64313141bfbece54fc063bea811011676 Mon Sep 17 00:00:00 2001 From: Kaede Hoshikawa Date: Sun, 1 May 2022 01:45:48 +0900 Subject: [PATCH 16/31] Add tests for use_transitive_state. --- .../hooks/use_transitive_state/feat_ssr.rs | 2 +- packages/yew/tests/use_prepared_state.rs | 1 + packages/yew/tests/use_transitive_state.rs | 63 +++++++++++++++++++ 3 files changed, 65 insertions(+), 1 deletion(-) create mode 100644 packages/yew/tests/use_transitive_state.rs diff --git a/packages/yew/src/functional/hooks/use_transitive_state/feat_ssr.rs b/packages/yew/src/functional/hooks/use_transitive_state/feat_ssr.rs index 1b7751b9858..74305caf86a 100644 --- a/packages/yew/src/functional/hooks/use_transitive_state/feat_ssr.rs +++ b/packages/yew/src/functional/hooks/use_transitive_state/feat_ssr.rs @@ -29,7 +29,7 @@ where let f = self.state_fn.borrow_mut().take().unwrap(); let state = f(&self.deps); - let state = bincode::serialize(&(&state, &self.deps)).expect("failed to prepare state"); + let state = bincode::serialize(&(Some(&state), Some(&self.deps))).expect("failed to prepare state"); Base64::encode_string(&state) } diff --git a/packages/yew/tests/use_prepared_state.rs b/packages/yew/tests/use_prepared_state.rs index 63670d7ad70..6e160593792 100644 --- a/packages/yew/tests/use_prepared_state.rs +++ b/packages/yew/tests/use_prepared_state.rs @@ -1,3 +1,4 @@ +#![cfg(target_arch = "wasm32")] #![cfg(feature = "hydration")] use std::time::Duration; diff --git a/packages/yew/tests/use_transitive_state.rs b/packages/yew/tests/use_transitive_state.rs new file mode 100644 index 00000000000..2b40f70b072 --- /dev/null +++ b/packages/yew/tests/use_transitive_state.rs @@ -0,0 +1,63 @@ +#![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() -> Html { + let ctr = use_transitive_state!(|_| -> u32 { 12345 }, ()).unwrap_or_default(); + + html! { +
+ {*ctr} +
+ } + } + + #[function_component] + fn App() -> Html { + html! { +
+ +
+ } + } + + let s = ServerRenderer::::new().render().await; + + assert_eq!( + s, + // div text content should be 0 but state should be 12345. + r#"
0
"# + ); + + gloo::utils::document() + .query_selector("#output") + .unwrap() + .unwrap() + .set_inner_html(&s); + + sleep(Duration::ZERO).await; + + Renderer::::with_root(gloo_utils::document().get_element_by_id("output").unwrap()) + .hydrate(); + + sleep(Duration::ZERO).await; + + let result = obtain_result_by_id("output"); + + // no placeholders, hydration is successful and div text content now becomes 12345. + assert_eq!(result, r#"
12345
"#); +} From ee5caea8abfc4ddfc4f24bac64110911f2ca3956 Mon Sep 17 00:00:00 2001 From: Kaede Hoshikawa Date: Sun, 1 May 2022 02:09:38 +0900 Subject: [PATCH 17/31] Fix cargo fmt. --- packages/yew-macro/src/use_prepared_state.rs | 2 +- packages/yew-macro/src/use_transitive_state.rs | 2 +- .../yew/src/functional/hooks/use_transitive_state/feat_ssr.rs | 3 ++- rustfmt.toml | 1 - 4 files changed, 4 insertions(+), 4 deletions(-) diff --git a/packages/yew-macro/src/use_prepared_state.rs b/packages/yew-macro/src/use_prepared_state.rs index 230512d9598..ba8109f7c6e 100644 --- a/packages/yew-macro/src/use_prepared_state.rs +++ b/packages/yew-macro/src/use_prepared_state.rs @@ -21,7 +21,7 @@ impl Parse for PreparedState ReturnType::Default => { return Err(syn::Error::new_spanned( &closure, - "You must specify a return type for this closure.This is used when the \ + "You must specify a return type for this closure. This is used when the \ closure is omitted from the client side rendering bundle.", )) } diff --git a/packages/yew-macro/src/use_transitive_state.rs b/packages/yew-macro/src/use_transitive_state.rs index bdcca330213..f7fde858c29 100644 --- a/packages/yew-macro/src/use_transitive_state.rs +++ b/packages/yew-macro/src/use_transitive_state.rs @@ -21,7 +21,7 @@ impl Parse for TransitiveState { ReturnType::Default => { return Err(syn::Error::new_spanned( &closure, - "You must specify a return type for this closure.This is used when the \ + "You must specify a return type for this closure. This is used when the \ closure is omitted from the client side rendering bundle.", )) } diff --git a/packages/yew/src/functional/hooks/use_transitive_state/feat_ssr.rs b/packages/yew/src/functional/hooks/use_transitive_state/feat_ssr.rs index 74305caf86a..9d680988e84 100644 --- a/packages/yew/src/functional/hooks/use_transitive_state/feat_ssr.rs +++ b/packages/yew/src/functional/hooks/use_transitive_state/feat_ssr.rs @@ -29,7 +29,8 @@ where 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"); + let state = + bincode::serialize(&(Some(&state), Some(&self.deps))).expect("failed to prepare state"); Base64::encode_string(&state) } diff --git a/rustfmt.toml b/rustfmt.toml index d3692036517..da3980084a9 100644 --- a/rustfmt.toml +++ b/rustfmt.toml @@ -12,4 +12,3 @@ group_imports = "StdExternalCrate" imports_granularity = "Module" reorder_impl_items = true use_field_init_shorthand = true -edition = "2021" From 1ff916abe2fd36c36832a5d95a78328add8f3ce7 Mon Sep 17 00:00:00 2001 From: Kaede Hoshikawa Date: Sun, 1 May 2022 02:24:40 +0900 Subject: [PATCH 18/31] Fix rustdoc. --- .../yew/src/functional/hooks/use_prepared_state/mod.rs | 9 ++++++--- .../yew/src/functional/hooks/use_transitive_state/mod.rs | 3 ++- 2 files changed, 8 insertions(+), 4 deletions(-) diff --git a/packages/yew/src/functional/hooks/use_prepared_state/mod.rs b/packages/yew/src/functional/hooks/use_prepared_state/mod.rs index 819c2751ed1..fd1b4946b5a 100644 --- a/packages/yew/src/functional/hooks/use_prepared_state/mod.rs +++ b/packages/yew/src/functional/hooks/use_prepared_state/mod.rs @@ -31,7 +31,8 @@ pub use feat_ssr::*; /// /// ``` /// # use yew::prelude::*; -/// # use serde::{Serialize, DeserializeOwned}; +/// # use serde::de::DeserializeOwned; +/// # use serde::Serialize; /// # use std::rc::Rc; /// #[hook] /// pub fn use_prepared_state(f: F, deps: D) -> Option> @@ -49,14 +50,16 @@ pub use feat_ssr::*; /// /// ``` /// # use yew::prelude::*; -/// # use serde::{Serialize, DeserializeOwned}; +/// # use serde::de::DeserializeOwned; +/// # use serde::Serialize; +/// # use yew::suspense::SuspensionResult; /// # use std::rc::Rc; /// # use std::future::Future; /// #[hook] /// pub fn use_prepared_state( /// f: F, /// deps: D, -/// ) -> impl Hook>>> +/// ) -> SuspensionResult>> /// where /// D: Serialize + DeserializeOwned + PartialEq + 'static, /// T: Serialize + DeserializeOwned + 'static, diff --git a/packages/yew/src/functional/hooks/use_transitive_state/mod.rs b/packages/yew/src/functional/hooks/use_transitive_state/mod.rs index a3791b1b924..492f8cd628b 100644 --- a/packages/yew/src/functional/hooks/use_transitive_state/mod.rs +++ b/packages/yew/src/functional/hooks/use_transitive_state/mod.rs @@ -31,7 +31,8 @@ pub use feat_ssr::*; /// /// ``` /// # use yew::prelude::*; -/// # use serde::{Serialize, DeserializeOwned}; +/// # use serde::de::DeserializeOwned; +/// # use serde::Serialize; /// # use std::rc::Rc; /// #[hook] /// pub fn use_transitive_state(f: F, deps: D) -> Option> From 59f0a0ba5bbd5d9c29fd79dca54b53bcb6156574 Mon Sep 17 00:00:00 2001 From: Kaede Hoshikawa Date: Wed, 4 May 2022 14:51:45 +0900 Subject: [PATCH 19/31] Asynchronously decode data during hydration. --- packages/yew-macro/src/lib.rs | 10 +- packages/yew-macro/src/use_prepared_state.rs | 25 +-- packages/yew/Cargo.toml | 7 +- packages/yew/src/functional/hooks/mod.rs | 8 + .../use_prepared_state/feat_hydration.rs | 90 ++++++++--- .../use_prepared_state/feat_hydration_ssr.rs | 82 +++++----- .../hooks/use_prepared_state/feat_none.rs | 4 +- .../hooks/use_prepared_state/feat_ssr.rs | 142 +++++++++--------- .../hooks/use_prepared_state/mod.rs | 39 ++--- .../feat_hydration_ssr.rs | 8 +- .../hooks/use_transitive_state/feat_ssr.rs | 10 +- .../hooks/use_transitive_state/mod.rs | 1 + packages/yew/src/functional/mod.rs | 28 +++- packages/yew/tests/hydration.rs | 1 + packages/yew/tests/use_prepared_state.rs | 22 +-- packages/yew/tests/use_transitive_state.rs | 20 +-- 16 files changed, 267 insertions(+), 230 deletions(-) diff --git a/packages/yew-macro/src/lib.rs b/packages/yew-macro/src/lib.rs index 44966eabad0..e5a50ab2250 100644 --- a/packages/yew-macro/src/lib.rs +++ b/packages/yew-macro/src/lib.rs @@ -157,19 +157,13 @@ pub fn hook(attr: TokenStream, item: TokenStream) -> proc_macro::TokenStream { #[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_with_closure_and_suspension(input: TokenStream) -> TokenStream { - let prepared_state = parse_macro_input!(input as PreparedState); + 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); + let prepared_state = parse_macro_input!(input as PreparedState); prepared_state.to_token_stream_without_closure().into() } diff --git a/packages/yew-macro/src/use_prepared_state.rs b/packages/yew-macro/src/use_prepared_state.rs index ba8109f7c6e..dda0f8fdade 100644 --- a/packages/yew-macro/src/use_prepared_state.rs +++ b/packages/yew-macro/src/use_prepared_state.rs @@ -4,13 +4,13 @@ use syn::parse::{Parse, ParseStream}; use syn::{parse_quote, Expr, ExprClosure, ReturnType, Token, Type}; #[derive(Debug)] -pub struct PreparedState { +pub struct PreparedState { closure: ExprClosure, return_type: Type, deps: Expr, } -impl Parse for PreparedState { +impl Parse for PreparedState { fn parse(input: ParseStream) -> syn::Result { // Reads a closure. let closure: ExprClosure = input.parse()?; @@ -39,16 +39,6 @@ impl Parse for PreparedState } } - if !WITH_ASYNC_CLOSURE { - if let Some(m) = closure.asyncness.as_ref() { - return Err(syn::Error::new_spanned( - &m, - "You need to enable feature tokio to use async closure under non-wasm32 \ - targets.", - )); - } - } - Ok(Self { closure, return_type, @@ -57,7 +47,7 @@ impl Parse for PreparedState } } -impl PreparedState { +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 { @@ -109,13 +99,8 @@ impl PreparedState { let deps = &self.deps; let rt = &self.return_type; - match &self.closure.asyncness { - Some(_) => quote! { - ::yew::functional::use_prepared_state_with_suspension::<#rt, _>(#deps) - }, - None => quote! { - ::yew::functional::use_prepared_state::<#rt, _>(#deps) - }, + quote! { + ::yew::functional::use_prepared_state::<#rt, _>(#deps) } } } diff --git a/packages/yew/Cargo.toml b/packages/yew/Cargo.toml index 72751dfd8b2..26ac812d28c 100644 --- a/packages/yew/Cargo.toml +++ b/packages/yew/Cargo.toml @@ -32,6 +32,7 @@ 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"] } +wasm-bindgen-futures = "0.4" [dependencies.web-sys] version = "0.3" @@ -67,10 +68,6 @@ features = [ "HtmlScriptElement", ] -[target.'cfg(target_arch = "wasm32")'.dependencies] -# we move it here so no promise-based spawn_local can present for -# non-wasm32 targets. -wasm-bindgen-futures = "0.4" [target.'cfg(not(target_arch = "wasm32"))'.dependencies] tokio = { version = "1.15.0", features = ["rt"], optional = true } @@ -93,7 +90,7 @@ features = [ [features] ssr = ["futures", "html-escape", "base64ct", "bincode"] csr = [] -hydration = ["csr", "base64ct", "bincode"] +hydration = ["csr", "bincode"] trace_hydration = ["hydration"] doc_test = ["csr", "hydration", "ssr"] wasm_test = ["csr", "hydration", "ssr"] diff --git a/packages/yew/src/functional/hooks/mod.rs b/packages/yew/src/functional/hooks/mod.rs index 3fd8c70cde0..ed03e423cf0 100644 --- a/packages/yew/src/functional/hooks/mod.rs +++ b/packages/yew/src/functional/hooks/mod.rs @@ -3,10 +3,14 @@ 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::*; @@ -14,10 +18,14 @@ 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::{AnyScope, HookContext}; diff --git a/packages/yew/src/functional/hooks/use_prepared_state/feat_hydration.rs b/packages/yew/src/functional/hooks/use_prepared_state/feat_hydration.rs index e5d0b4ac9d3..5651d8c280f 100644 --- a/packages/yew/src/functional/hooks/use_prepared_state/feat_hydration.rs +++ b/packages/yew/src/functional/hooks/use_prepared_state/feat_hydration.rs @@ -3,17 +3,20 @@ use std::marker::PhantomData; use std::rc::Rc; +use gloo_utils::window; +use js_sys::Uint8Array; use serde::de::DeserializeOwned; use serde::Serialize; +use wasm_bindgen::JsCast; +use wasm_bindgen_futures::JsFuture; use super::PreparedStateBase; -use crate::functional::{Hook, HookContext}; -use crate::hook; -use crate::suspense::SuspensionResult; +use crate::functional::{use_state, Hook, HookContext}; +use crate::io_coop::spawn_local; +use crate::suspense::{Suspension, SuspensionResult}; -/// The client-side rendering variant. This is used for client side rendering. #[doc(hidden)] -pub fn use_prepared_state(deps: D) -> impl Hook>> +pub fn use_prepared_state(deps: D) -> impl Hook>>> where D: Serialize + DeserializeOwned + PartialEq + 'static, T: Serialize + DeserializeOwned + 'static, @@ -32,22 +35,70 @@ where D: Serialize + DeserializeOwned + PartialEq + 'static, T: Serialize + DeserializeOwned + 'static, { - type Output = Option>; + type Output = SuspensionResult>>; fn run(self, ctx: &mut HookContext) -> Self::Output { - let state = ctx.next_prepared_state(|_re_render, buf| -> PreparedStateBase { - buf.map(|buf| PreparedStateBase::::decode(buf)) - .unwrap_or(PreparedStateBase { + let data = use_state(|| { + let (s, handle) = Suspension::new(); + ( + SuspensionResult::<(Option>, Option>)>::Err(s), + Some(handle), + ) + }) + .run(ctx); + + let state = { + let data = data.clone(); + ctx.next_prepared_state(move |_re_render, buf| -> PreparedStateBase { + if let Some(buf) = buf { + let buf = format!("data:application/octet-binary;base64,{}", buf); + + spawn_local(async move { + let fetch_promise = window().fetch_with_str(&buf); + + let content_promise = JsFuture::from(fetch_promise) + .await + .and_then(|m| m.dyn_into::()) + .and_then(|m| m.array_buffer()) + .expect("failed to decode prepared state"); + + let content_array = JsFuture::from(content_promise) + .await + .as_ref() + .map(Uint8Array::new) + .expect("failed to decode prepared state"); + + let (state, deps) = bincode::deserialize::<(Option, Option)>( + &content_array.to_vec(), + ) + .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, - }) - }); - if state.deps.as_deref() == Some(&self.deps) { - return state.state.clone(); + 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); + } } - None + Ok(None) } } @@ -56,14 +107,3 @@ where deps, } } - -/// The with suspension variant for use_prepared_state_on_client_side. -#[doc(hidden)] -#[hook] -pub fn use_prepared_state_with_suspension(deps: D) -> SuspensionResult>> -where - D: Serialize + DeserializeOwned + PartialEq + 'static, - T: Serialize + DeserializeOwned + 'static, -{ - Ok(use_prepared_state(deps)) -} diff --git a/packages/yew/src/functional/hooks/use_prepared_state/feat_hydration_ssr.rs b/packages/yew/src/functional/hooks/use_prepared_state/feat_hydration_ssr.rs index e96ba8ea408..be7ffab2f04 100644 --- a/packages/yew/src/functional/hooks/use_prepared_state/feat_hydration_ssr.rs +++ b/packages/yew/src/functional/hooks/use_prepared_state/feat_hydration_ssr.rs @@ -1,5 +1,6 @@ //! The client-and-server-side rendering variant. +use std::future::Future; use std::rc::Rc; use serde::de::DeserializeOwned; @@ -8,9 +9,13 @@ 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(f: F, deps: D) -> impl Hook>> +pub fn use_prepared_state( + f: F, + deps: D, +) -> impl Hook>>> where D: Serialize + DeserializeOwned + PartialEq + 'static, T: Serialize + DeserializeOwned + 'static, @@ -32,7 +37,7 @@ where T: Serialize + DeserializeOwned + 'static, F: FnOnce(&D) -> T, { - type Output = Option>; + type Output = SuspensionResult>>; fn run(self, ctx: &mut HookContext) -> Self::Output { match ctx.mode { @@ -45,59 +50,46 @@ where HookProvider:: { deps, f } } -#[cfg_attr(documenting, doc(cfg(any(target_arch = "wasm32", feature = "tokio"))))] -#[cfg(any(target_arch = "wasm32", feature = "tokio"))] -mod feat_io { - use std::future::Future; - - use super::*; - use crate::suspense::SuspensionResult; - - #[doc(hidden)] - pub fn use_prepared_state_with_suspension( - f: F, - deps: D, - ) -> impl Hook>>> +#[doc(hidden)] +pub fn use_prepared_state_with_suspension( + f: F, + deps: D, +) -> impl Hook>>> +where + D: Serialize + DeserializeOwned + PartialEq + 'static, + T: Serialize + DeserializeOwned + 'static, + F: FnOnce(&D) -> U, + U: 'static + Future, +{ + struct HookProvider where D: Serialize + DeserializeOwned + PartialEq + 'static, T: Serialize + DeserializeOwned + 'static, F: FnOnce(&D) -> U, U: 'static + Future, { - struct HookProvider - where - D: Serialize + DeserializeOwned + PartialEq + 'static, - T: Serialize + DeserializeOwned + 'static, - F: FnOnce(&D) -> U, - U: 'static + Future, - { - deps: D, - f: F, - } + deps: D, + f: F, + } - impl Hook for HookProvider - where - D: Serialize + DeserializeOwned + PartialEq + 'static, - T: Serialize + DeserializeOwned + 'static, - F: FnOnce(&D) -> U, - U: 'static + Future, - { - type Output = SuspensionResult>>; + impl Hook for HookProvider + where + D: Serialize + DeserializeOwned + PartialEq + 'static, + T: Serialize + DeserializeOwned + 'static, + F: FnOnce(&D) -> U, + U: 'static + Future, + { + type Output = SuspensionResult>>; - 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_with_suspension(self.deps).run(ctx), + 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:: { deps, f } } -} -#[cfg_attr(documenting, doc(cfg(any(target_arch = "wasm32", feature = "tokio"))))] -#[cfg(any(target_arch = "wasm32", feature = "tokio"))] -pub use feat_io::*; + HookProvider:: { deps, f } +} diff --git a/packages/yew/src/functional/hooks/use_prepared_state/feat_none.rs b/packages/yew/src/functional/hooks/use_prepared_state/feat_none.rs index 988d1f92d2e..b1fb795e458 100644 --- a/packages/yew/src/functional/hooks/use_prepared_state/feat_none.rs +++ b/packages/yew/src/functional/hooks/use_prepared_state/feat_none.rs @@ -10,12 +10,12 @@ use crate::suspense::SuspensionResult; #[doc(hidden)] #[hook] -pub fn use_prepared_state(_deps: D) -> Option> +pub fn use_prepared_state(_deps: D) -> SuspensionResult>> where D: Serialize + DeserializeOwned + PartialEq + 'static, T: Serialize + DeserializeOwned + 'static, { - None + Ok(None) } #[doc(hidden)] diff --git a/packages/yew/src/functional/hooks/use_prepared_state/feat_ssr.rs b/packages/yew/src/functional/hooks/use_prepared_state/feat_ssr.rs index 26923d3295e..47d08179e4c 100644 --- a/packages/yew/src/functional/hooks/use_prepared_state/feat_ssr.rs +++ b/packages/yew/src/functional/hooks/use_prepared_state/feat_ssr.rs @@ -1,15 +1,22 @@ //! 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, Hook, HookContext}; +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(f: F, deps: D) -> impl Hook>> +pub fn use_prepared_state( + f: F, + deps: D, +) -> impl Hook>>> where D: Serialize + DeserializeOwned + PartialEq + 'static, T: Serialize + DeserializeOwned + 'static, @@ -31,7 +38,7 @@ where T: Serialize + DeserializeOwned + 'static, F: FnOnce(&D) -> T, { - type Output = Option>; + type Output = SuspensionResult>>; fn run(self, ctx: &mut HookContext) -> Self::Output { let f = self.f; @@ -45,101 +52,92 @@ where 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 { state }); - state.state.clone() + Ok(state.state.clone()) } } HookProvider:: { deps, f } } -#[cfg_attr(documenting, doc(cfg(any(target_arch = "wasm32", feature = "tokio"))))] -#[cfg(any(target_arch = "wasm32", feature = "tokio"))] -mod feat_io { - use std::future::Future; - - use super::*; - use crate::functional::use_state; - use crate::io_coop::spawn_local; - use crate::suspense::{Suspension, SuspensionResult}; - - #[doc(hidden)] - pub fn use_prepared_state_with_suspension( - f: F, +#[doc(hidden)] +pub fn use_prepared_state_with_suspension( + f: F, + deps: D, +) -> impl Hook>>> +where + D: Serialize + DeserializeOwned + PartialEq + 'static, + T: Serialize + DeserializeOwned + 'static, + F: FnOnce(&D) -> U, + U: 'static + Future, +{ + struct HookProvider + where + D: Serialize + DeserializeOwned + PartialEq + 'static, + T: Serialize + DeserializeOwned + 'static, + F: FnOnce(&D) -> U, + U: 'static + Future, + { deps: D, - ) -> impl Hook>>> + f: F, + } + + impl Hook for HookProvider where D: Serialize + DeserializeOwned + PartialEq + 'static, T: Serialize + DeserializeOwned + 'static, F: FnOnce(&D) -> U, U: 'static + Future, { - struct HookProvider - where - D: Serialize + DeserializeOwned + PartialEq + 'static, - T: Serialize + DeserializeOwned + 'static, - F: FnOnce(&D) -> U, - U: 'static + Future, - { - deps: D, - f: F, - } + type Output = SuspensionResult>>; - impl Hook for HookProvider - where - D: Serialize + DeserializeOwned + PartialEq + 'static, - T: Serialize + DeserializeOwned + 'static, - F: FnOnce(&D) -> U, - U: 'static + Future, - { - type Output = SuspensionResult>>; - - 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); + 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); + { + 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)); - }) + spawn_local(async move { + let state = state_f.await; + result.set((Ok(Rc::new(state)), None)); }) - .run(ctx); - } + }) + .run(ctx); + } - let state = result.0.clone()?; + let state = result.0.clone()?; - let state = PreparedStateBase { - state: Some(state), - deps: Some(deps), - }; + 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 { state }); + let state = + ctx.next_prepared_state(|_re_render, _| -> PreparedStateBase { state }); - Ok(state.state.clone()) - } + Ok(state.state.clone()) } - - HookProvider:: { deps, f } } -} -#[cfg_attr(documenting, doc(cfg(any(target_arch = "wasm32", feature = "tokio"))))] -#[cfg(any(target_arch = "wasm32", feature = "tokio"))] -pub use feat_io::*; + HookProvider:: { deps, f } +} diff --git a/packages/yew/src/functional/hooks/use_prepared_state/mod.rs b/packages/yew/src/functional/hooks/use_prepared_state/mod.rs index fd1b4946b5a..c8c331d6423 100644 --- a/packages/yew/src/functional/hooks/use_prepared_state/mod.rs +++ b/packages/yew/src/functional/hooks/use_prepared_state/mod.rs @@ -29,13 +29,16 @@ pub use feat_ssr::*; /// /// `let state = use_prepared_state!(|deps| -> ReturnType { ... }, deps);` /// +/// It has the following signature: +/// /// ``` /// # use yew::prelude::*; /// # use serde::de::DeserializeOwned; /// # use serde::Serialize; /// # use std::rc::Rc; +/// # use yew::suspense::SuspensionResult; /// #[hook] -/// pub fn use_prepared_state(f: F, deps: D) -> Option> +/// pub fn use_prepared_state(f: F, deps: D) -> SuspensionResult>> /// where /// D: Serialize + DeserializeOwned + PartialEq + 'static, /// T: Serialize + DeserializeOwned + 'static, @@ -78,15 +81,12 @@ pub use feat_ssr::*; /// 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_prepared_state_macro as use_prepared_state; -// With SSR, but no runtime available. +// With SSR. #[doc(hidden)] -#[cfg(all(feature = "ssr", not(any(target_arch = "wasm32", feature = "tokio"))))] +#[cfg(feature = "ssr")] pub use yew_macro::use_prepared_state_with_closure as use_prepared_state_macro; -// With SSR, and runtime is available. -#[doc(hidden)] -#[cfg(all(feature = "ssr", any(target_arch = "wasm32", feature = "tokio")))] -pub use yew_macro::use_prepared_state_with_closure_and_suspension as use_prepared_state_macro; // Without SSR. #[doc(hidden)] #[cfg(not(feature = "ssr"))] @@ -94,6 +94,7 @@ pub use yew_macro::use_prepared_state_without_closure as use_prepared_state_macr #[cfg(any(feature = "hydration", feature = "ssr"))] mod feat_any_hydration_ssr { + use std::marker::PhantomData; use std::rc::Rc; use base64ct::{Base64, Encoding}; @@ -107,27 +108,13 @@ mod feat_any_hydration_ssr { D: Serialize + DeserializeOwned + PartialEq + 'static, T: Serialize + DeserializeOwned + 'static, { + #[cfg(feature = "ssr")] pub state: Option>, + #[cfg(feature = "ssr")] pub deps: Option>, - } - - #[cfg(feature = "hydration")] - impl PreparedStateBase - where - D: Serialize + DeserializeOwned + PartialEq + 'static, - T: Serialize + DeserializeOwned + 'static, - { - pub fn decode(s: &str) -> Self { - let buf = Base64::decode_vec(s).unwrap(); - - let (state, deps) = bincode::deserialize::<(Option, Option)>(&buf) - .expect("failed to deserialize state"); - - PreparedStateBase { - state: state.map(Rc::new), - deps: deps.map(Rc::new), - } - } + #[cfg(feature = "hydration")] + pub has_buf: bool, + pub _marker: PhantomData<(T, D)>, } impl PreparedState for PreparedStateBase diff --git a/packages/yew/src/functional/hooks/use_transitive_state/feat_hydration_ssr.rs b/packages/yew/src/functional/hooks/use_transitive_state/feat_hydration_ssr.rs index 365eeb2d84e..c0ed6f9edda 100644 --- a/packages/yew/src/functional/hooks/use_transitive_state/feat_hydration_ssr.rs +++ b/packages/yew/src/functional/hooks/use_transitive_state/feat_hydration_ssr.rs @@ -8,9 +8,13 @@ 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(f: F, deps: D) -> impl Hook>> +pub fn use_transitive_state( + f: F, + deps: D, +) -> impl Hook>>> where D: Serialize + DeserializeOwned + PartialEq + 'static, T: Serialize + DeserializeOwned + 'static, @@ -32,7 +36,7 @@ where T: Serialize + DeserializeOwned + 'static, F: 'static + FnOnce(&D) -> T, { - type Output = Option>; + type Output = SuspensionResult>>; fn run(self, ctx: &mut HookContext) -> Self::Output { match ctx.mode { diff --git a/packages/yew/src/functional/hooks/use_transitive_state/feat_ssr.rs b/packages/yew/src/functional/hooks/use_transitive_state/feat_ssr.rs index 9d680988e84..fdb35bef800 100644 --- a/packages/yew/src/functional/hooks/use_transitive_state/feat_ssr.rs +++ b/packages/yew/src/functional/hooks/use_transitive_state/feat_ssr.rs @@ -8,6 +8,7 @@ use serde::de::DeserializeOwned; use serde::Serialize; use crate::functional::{Hook, HookContext, PreparedState}; +use crate::suspense::SuspensionResult; pub(super) struct TransitiveStateBase where @@ -37,7 +38,10 @@ where } #[doc(hidden)] -pub fn use_transitive_state(f: F, deps: D) -> impl Hook>> +pub fn use_transitive_state( + f: F, + deps: D, +) -> impl Hook>>> where D: Serialize + DeserializeOwned + PartialEq + 'static, T: Serialize + DeserializeOwned + 'static, @@ -59,7 +63,7 @@ where T: Serialize + DeserializeOwned + 'static, F: 'static + FnOnce(&D) -> T, { - type Output = Option>; + type Output = SuspensionResult>>; fn run(self, ctx: &mut HookContext) -> Self::Output { let f = self.f; @@ -71,7 +75,7 @@ where } }); - None + Ok(None) } } diff --git a/packages/yew/src/functional/hooks/use_transitive_state/mod.rs b/packages/yew/src/functional/hooks/use_transitive_state/mod.rs index 492f8cd628b..77376124de2 100644 --- a/packages/yew/src/functional/hooks/use_transitive_state/mod.rs +++ b/packages/yew/src/functional/hooks/use_transitive_state/mod.rs @@ -51,6 +51,7 @@ pub use feat_ssr::*; /// 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)] diff --git a/packages/yew/src/functional/mod.rs b/packages/yew/src/functional/mod.rs index 91c4ae1111e..094a995388d 100644 --- a/packages/yew/src/functional/mod.rs +++ b/packages/yew/src/functional/mod.rs @@ -27,6 +27,7 @@ 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}; @@ -67,6 +68,7 @@ pub use yew_macro::hook; type ReRender = Rc; /// 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")] @@ -81,6 +83,7 @@ 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, @@ -88,11 +91,14 @@ pub struct HookContext { states: Vec>, effects: Vec>, + #[cfg(any(target_arch = "wasm32", feature = "tokio"))] #[cfg(any(feature = "hydration", feature = "ssr"))] prepared_states: Vec>, + #[cfg(any(target_arch = "wasm32", feature = "tokio"))] #[cfg(feature = "hydration")] prepared_states_data: Vec>, + #[cfg(any(target_arch = "wasm32", feature = "tokio"))] #[cfg(feature = "hydration")] prepared_state_counter: usize, @@ -105,22 +111,29 @@ impl HookContext { fn new( scope: AnyScope, re_render: ReRender, - #[cfg(all(feature = "hydration", feature = "ssr"))] mode: RenderMode, - #[cfg(feature = "hydration")] prepared_state: Option<&str>, + #[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 { RefCell::new(HookContext { 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 { @@ -128,6 +141,7 @@ impl HookContext { None => Vec::new(), } }, + #[cfg(any(target_arch = "wasm32", feature = "tokio"))] #[cfg(feature = "hydration")] prepared_state_counter: 0, @@ -173,6 +187,7 @@ impl HookContext { t } + #[cfg(any(target_arch = "wasm32", feature = "tokio"))] #[cfg(any(feature = "hydration", feature = "ssr"))] pub(crate) fn next_prepared_state( &mut self, @@ -205,6 +220,7 @@ impl HookContext { #[inline(always)] fn prepare_run(&mut self) { + #[cfg(any(target_arch = "wasm32", feature = "tokio"))] #[cfg(feature = "hydration")] { self.prepared_state_counter = 0; @@ -261,11 +277,15 @@ impl HookContext { } } - #[cfg(not(feature = "ssr"))] + #[cfg(any( + not(feature = "ssr"), + not(any(target_arch = "wasm32", feature = "tokio")) + ))] fn prepare_state(&self) -> Option { None } + #[cfg(any(target_arch = "wasm32", feature = "tokio"))] #[cfg(feature = "ssr")] fn prepare_state(&self) -> Option { if self.prepared_states.is_empty() { @@ -341,8 +361,10 @@ where 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(), ), diff --git a/packages/yew/tests/hydration.rs b/packages/yew/tests/hydration.rs index 8b06f5ba9b8..b0d249015fe 100644 --- a/packages/yew/tests/hydration.rs +++ b/packages/yew/tests/hydration.rs @@ -1,4 +1,5 @@ #![cfg(feature = "hydration")] +#![cfg(target_arch = "wasm32")] use std::rc::Rc; use std::time::Duration; diff --git a/packages/yew/tests/use_prepared_state.rs b/packages/yew/tests/use_prepared_state.rs index 6e160593792..93c52e71b06 100644 --- a/packages/yew/tests/use_prepared_state.rs +++ b/packages/yew/tests/use_prepared_state.rs @@ -16,22 +16,24 @@ wasm_bindgen_test::wasm_bindgen_test_configure!(run_in_browser); #[wasm_bindgen_test] async fn use_prepared_state_works() { #[function_component] - fn Comp() -> Html { - let ctr = use_prepared_state!(|_| -> u32 { 12345 }, ()).unwrap_or_default(); + fn Comp() -> HtmlResult { + let ctr = use_prepared_state!(|_| -> u32 { 12345 }, ())?.unwrap_or_default(); - html! { + Ok(html! {
{*ctr}
- } + }) } #[function_component] fn App() -> Html { html! { -
- -
+ +
+ +
+
} } @@ -39,7 +41,7 @@ async fn use_prepared_state_works() { assert_eq!( s, - r#"
12345
"# + r#"
12345
"# ); gloo::utils::document() @@ -53,7 +55,7 @@ async fn use_prepared_state_works() { Renderer::::with_root(gloo_utils::document().get_element_by_id("output").unwrap()) .hydrate(); - sleep(Duration::ZERO).await; + sleep(Duration::from_millis(100)).await; let result = obtain_result_by_id("output"); @@ -103,7 +105,7 @@ async fn use_prepared_state_with_suspension_works() { Renderer::::with_root(gloo_utils::document().get_element_by_id("output").unwrap()) .hydrate(); - sleep(Duration::ZERO).await; + sleep(Duration::from_millis(100)).await; let result = obtain_result_by_id("output"); diff --git a/packages/yew/tests/use_transitive_state.rs b/packages/yew/tests/use_transitive_state.rs index 2b40f70b072..3e180d2187d 100644 --- a/packages/yew/tests/use_transitive_state.rs +++ b/packages/yew/tests/use_transitive_state.rs @@ -16,22 +16,24 @@ wasm_bindgen_test::wasm_bindgen_test_configure!(run_in_browser); #[wasm_bindgen_test] async fn use_transitive_state_works() { #[function_component] - fn Comp() -> Html { - let ctr = use_transitive_state!(|_| -> u32 { 12345 }, ()).unwrap_or_default(); + fn Comp() -> HtmlResult { + let ctr = use_transitive_state!(|_| -> u32 { 12345 }, ())?.unwrap_or_default(); - html! { + Ok(html! {
{*ctr}
- } + }) } #[function_component] fn App() -> Html { html! { -
- -
+ +
+ +
+
} } @@ -40,7 +42,7 @@ async fn use_transitive_state_works() { assert_eq!( s, // div text content should be 0 but state should be 12345. - r#"
0
"# + r#"
0
"# ); gloo::utils::document() @@ -54,7 +56,7 @@ async fn use_transitive_state_works() { Renderer::::with_root(gloo_utils::document().get_element_by_id("output").unwrap()) .hydrate(); - sleep(Duration::ZERO).await; + sleep(Duration::from_millis(100)).await; let result = obtain_result_by_id("output"); From 566ecb83bed904211d06132450f2d0c3730d4d1a Mon Sep 17 00:00:00 2001 From: Kaede Hoshikawa Date: Wed, 4 May 2022 15:09:02 +0900 Subject: [PATCH 20/31] Fix feature flags. --- examples/simple_ssr/Cargo.toml | 6 ++- examples/simple_ssr/README.md | 2 +- examples/simple_ssr/index.html | 2 +- examples/simple_ssr/src/lib.rs | 1 + .../use_prepared_state/feat_hydration.rs | 41 +++++++++++-------- .../hooks/use_prepared_state/mod.rs | 4 +- 6 files changed, 34 insertions(+), 22 deletions(-) diff --git a/examples/simple_ssr/Cargo.toml b/examples/simple_ssr/Cargo.toml index c59ff92ce80..0e1fc0aad6d 100644 --- a/examples/simple_ssr/Cargo.toml +++ b/examples/simple_ssr/Cargo.toml @@ -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", "tokio"] } +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"] diff --git a/examples/simple_ssr/README.md b/examples/simple_ssr/README.md index 6c02a63edf3..41105b934dd 100644 --- a/examples/simple_ssr/README.md +++ b/examples/simple_ssr/README.md @@ -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 diff --git a/examples/simple_ssr/index.html b/examples/simple_ssr/index.html index 62951cf4073..2cb77d4d490 100644 --- a/examples/simple_ssr/index.html +++ b/examples/simple_ssr/index.html @@ -4,6 +4,6 @@ Yew SSR Example - + diff --git a/examples/simple_ssr/src/lib.rs b/examples/simple_ssr/src/lib.rs index f61e3f64009..dd291b644a8 100644 --- a/examples/simple_ssr/src/lib.rs +++ b/examples/simple_ssr/src/lib.rs @@ -7,6 +7,7 @@ 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(); diff --git a/packages/yew/src/functional/hooks/use_prepared_state/feat_hydration.rs b/packages/yew/src/functional/hooks/use_prepared_state/feat_hydration.rs index 5651d8c280f..94770b1b6e4 100644 --- a/packages/yew/src/functional/hooks/use_prepared_state/feat_hydration.rs +++ b/packages/yew/src/functional/hooks/use_prepared_state/feat_hydration.rs @@ -7,7 +7,7 @@ use gloo_utils::window; use js_sys::Uint8Array; use serde::de::DeserializeOwned; use serde::Serialize; -use wasm_bindgen::JsCast; +use wasm_bindgen::{JsCast, JsValue}; use wasm_bindgen_futures::JsFuture; use super::PreparedStateBase; @@ -15,6 +15,22 @@ use crate::functional::{use_state, Hook, HookContext}; use crate::io_coop::spawn_local; use crate::suspense::{Suspension, SuspensionResult}; +async fn decode_base64(s: &str) -> Result, JsValue> { + let fetch_promise = window().fetch_with_str(s); + + let content_promise = JsFuture::from(fetch_promise) + .await + .and_then(|m| m.dyn_into::()) + .and_then(|m| m.array_buffer())?; + + let content_array = JsFuture::from(content_promise) + .await + .as_ref() + .map(Uint8Array::new)?; + + Ok(content_array.to_vec()) +} + #[doc(hidden)] pub fn use_prepared_state(deps: D) -> impl Hook>>> where @@ -54,25 +70,14 @@ where let buf = format!("data:application/octet-binary;base64,{}", buf); spawn_local(async move { - let fetch_promise = window().fetch_with_str(&buf); - - let content_promise = JsFuture::from(fetch_promise) + let buf = decode_base64(&buf) .await - .and_then(|m| m.dyn_into::()) - .and_then(|m| m.array_buffer()) - .expect("failed to decode prepared state"); + .expect("failed to deserialize state"); - let content_array = JsFuture::from(content_promise) - .await - .as_ref() - .map(Uint8Array::new) - .expect("failed to decode prepared state"); - - let (state, deps) = bincode::deserialize::<(Option, Option)>( - &content_array.to_vec(), - ) - .map(|(state, deps)| (state.map(Rc::new), deps.map(Rc::new))) - .expect("failed to deserialize state"); + let (state, deps) = + bincode::deserialize::<(Option, Option)>(&buf) + .map(|(state, deps)| (state.map(Rc::new), deps.map(Rc::new))) + .expect("failed to deserialize state"); data.set((Ok((state, deps)), None)); }); diff --git a/packages/yew/src/functional/hooks/use_prepared_state/mod.rs b/packages/yew/src/functional/hooks/use_prepared_state/mod.rs index c8c331d6423..c0a4142c1e8 100644 --- a/packages/yew/src/functional/hooks/use_prepared_state/mod.rs +++ b/packages/yew/src/functional/hooks/use_prepared_state/mod.rs @@ -95,9 +95,9 @@ pub use yew_macro::use_prepared_state_without_closure as use_prepared_state_macr #[cfg(any(feature = "hydration", feature = "ssr"))] mod feat_any_hydration_ssr { use std::marker::PhantomData; + #[cfg(feature = "ssr")] use std::rc::Rc; - use base64ct::{Base64, Encoding}; use serde::de::DeserializeOwned; use serde::Serialize; @@ -124,6 +124,8 @@ mod feat_any_hydration_ssr { { #[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"); From 23446637a80a42339d77961cb9efe00f6e27f880 Mon Sep 17 00:00:00 2001 From: Kaede Hoshikawa Date: Wed, 4 May 2022 15:19:41 +0900 Subject: [PATCH 21/31] Fix docs. --- .../functional/hooks/use_prepared_state/mod.rs | 16 +++++++++++----- .../functional/hooks/use_transitive_state/mod.rs | 11 +++++++---- 2 files changed, 18 insertions(+), 9 deletions(-) diff --git a/packages/yew/src/functional/hooks/use_prepared_state/mod.rs b/packages/yew/src/functional/hooks/use_prepared_state/mod.rs index c0a4142c1e8..20e3fd8e39e 100644 --- a/packages/yew/src/functional/hooks/use_prepared_state/mod.rs +++ b/packages/yew/src/functional/hooks/use_prepared_state/mod.rs @@ -22,12 +22,12 @@ pub use feat_ssr::*; /// hydrated. /// /// It accepts a closure as the first argument and a dependency type as the second argument. -/// It returns `Option>`. +/// It returns `SuspensionResult>>`. /// -/// During hydration, it will only return `Some(Rc)` if the component is hydrated from a +/// During hydration, it will only return `Ok(Some(Rc))` if the component is hydrated from a /// server-side rendering artifact and its dependency value matches. /// -/// `let state = use_prepared_state!(|deps| -> ReturnType { ... }, deps);` +/// `let state = use_prepared_state!(|deps| -> ReturnType { ... }, deps)?;` /// /// It has the following signature: /// @@ -47,10 +47,11 @@ pub use feat_ssr::*; /// ``` /// /// The first argument can also be an [async closure](https://github.com/rust-lang/rust/issues/62290). -/// The hook will become a suspendible hook that returns `SuspensionResult>>`. /// /// `let state = use_prepared_state!(async |deps| -> ReturnType { ... }, deps)?;` /// +/// When accepting an async closure, it has the following signature: +/// /// ``` /// # use yew::prelude::*; /// # use serde::de::DeserializeOwned; @@ -71,7 +72,8 @@ pub use feat_ssr::*; /// # { todo!() } /// ``` /// -/// During server-side rending a value of type T will be calculated from the first closure. +/// 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. @@ -81,6 +83,10 @@ pub use feat_ssr::*; /// 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. diff --git a/packages/yew/src/functional/hooks/use_transitive_state/mod.rs b/packages/yew/src/functional/hooks/use_transitive_state/mod.rs index 77376124de2..0b2f3006158 100644 --- a/packages/yew/src/functional/hooks/use_transitive_state/mod.rs +++ b/packages/yew/src/functional/hooks/use_transitive_state/mod.rs @@ -20,22 +20,25 @@ pub use feat_ssr::*; /// 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 `Option>`. +/// It returns `SuspensionResult>>`. /// -/// It will always return `None` during server-side rendering. +/// It will always return `Ok(None)` during server-side rendering. /// -/// During hydration, it will only return `Some(Rc)` if the component is hydrated from a +/// During hydration, it will only return `Ok(Some(Rc))` 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 yew::prelude::*; /// # use serde::de::DeserializeOwned; /// # use serde::Serialize; /// # use std::rc::Rc; +/// # use yew::suspense::SuspensionResult; /// #[hook] -/// pub fn use_transitive_state(f: F, deps: D) -> Option> +/// pub fn use_transitive_state(f: F, deps: D) -> SuspensionResult>> /// where /// D: Serialize + DeserializeOwned + PartialEq + 'static, /// T: Serialize + DeserializeOwned + 'static, From 337a31b21ba0aa7a987b78404e943d96ea3d0ddf Mon Sep 17 00:00:00 2001 From: Kaede Hoshikawa Date: Wed, 4 May 2022 15:22:25 +0900 Subject: [PATCH 22/31] Feature flags on ssr_router. --- examples/ssr_router/Cargo.toml | 6 +++++- examples/ssr_router/README.md | 2 +- examples/ssr_router/index.html | 2 +- 3 files changed, 7 insertions(+), 3 deletions(-) diff --git a/examples/ssr_router/Cargo.toml b/examples/ssr_router/Cargo.toml index 49092a942f0..01ce860c945 100644 --- a/examples/ssr_router/Cargo.toml +++ b/examples/ssr_router/Cargo.toml @@ -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", "trace_hydration"] } +yew = { path = "../../packages/yew" } function_router = { path = "../function_router" } log = "0.4" @@ -22,3 +22,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"] diff --git a/examples/ssr_router/README.md b/examples/ssr_router/README.md index 0f65d5a50b9..013c3b63c4a 100644 --- a/examples/ssr_router/README.md +++ b/examples/ssr_router/README.md @@ -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 diff --git a/examples/ssr_router/index.html b/examples/ssr_router/index.html index 95eb7d33dd4..817504041ed 100644 --- a/examples/ssr_router/index.html +++ b/examples/ssr_router/index.html @@ -3,7 +3,7 @@ - + Yew • SSR Router Date: Wed, 4 May 2022 15:42:30 +0900 Subject: [PATCH 23/31] Adjust workflow to reflect feature flags. --- .github/workflows/main-checks.yml | 9 ++++----- 1 file changed, 4 insertions(+), 5 deletions(-) diff --git a/.github/workflows/main-checks.yml b/.github/workflows/main-checks.yml index 7cb47c12e94..376e32d7f27 100644 --- a/.github/workflows/main-checks.yml +++ b/.github/workflows/main-checks.yml @@ -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: | @@ -143,7 +142,7 @@ jobs: run: | geckodriver --version echo "" - which geckodriver + which geckodriver chromedriver --version echo "" which chromedriver @@ -192,7 +191,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 From 8503d2462e9343cf9383bf801072e7b0f92f8449 Mon Sep 17 00:00:00 2001 From: Kaede Hoshikawa Date: Sun, 22 May 2022 14:32:36 +0900 Subject: [PATCH 24/31] Fix features. --- examples/ssr_router/Cargo.toml | 2 +- packages/yew/Cargo.toml | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/examples/ssr_router/Cargo.toml b/examples/ssr_router/Cargo.toml index 73ec8210c8f..6cc7c73fda6 100644 --- a/examples/ssr_router/Cargo.toml +++ b/examples/ssr_router/Cargo.toml @@ -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" diff --git a/packages/yew/Cargo.toml b/packages/yew/Cargo.toml index 46c615aec9a..896fe2d5d98 100644 --- a/packages/yew/Cargo.toml +++ b/packages/yew/Cargo.toml @@ -92,7 +92,7 @@ features = [ # tokio = ["dep:tokio"] ssr = ["futures", "html-escape", "base64ct", "bincode"] # dep:html-escape csr = [] -hydration = ["csr"] +hydration = ["csr", "bincode"] default = [] [target.'cfg(not(target_arch = "wasm32"))'.dev-dependencies] From 1f0b5e1159b517c77f58e2e56919fe88e4879ea6 Mon Sep 17 00:00:00 2001 From: Kaede Hoshikawa Date: Mon, 23 May 2022 18:53:48 +0900 Subject: [PATCH 25/31] Restore wasm-bindgen-futures to be wasm32 only. --- packages/yew/Cargo.toml | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/packages/yew/Cargo.toml b/packages/yew/Cargo.toml index 896fe2d5d98..375f80ba415 100644 --- a/packages/yew/Cargo.toml +++ b/packages/yew/Cargo.toml @@ -32,7 +32,6 @@ 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"] } -wasm-bindgen-futures = "0.4" [dependencies.web-sys] version = "0.3" @@ -68,6 +67,10 @@ features = [ "HtmlScriptElement", ] + [target.'cfg(target_arch = "wasm32")'.dependencies] + # we move it here so no promise-based spawn_local can present for + # non-wasm32 targets. + wasm-bindgen-futures = "0.4" [target.'cfg(not(target_arch = "wasm32"))'.dependencies] tokio = { version = "1.15.0", features = ["rt"], optional = true } From 268cf5761ae4eff525c642be3a4f7b2a9c59a643 Mon Sep 17 00:00:00 2001 From: Kaede Hoshikawa Date: Mon, 23 May 2022 19:01:20 +0900 Subject: [PATCH 26/31] Revert wasm-bindgen-futures. --- packages/yew/Cargo.toml | 6 +----- 1 file changed, 1 insertion(+), 5 deletions(-) diff --git a/packages/yew/Cargo.toml b/packages/yew/Cargo.toml index 375f80ba415..78872fb50b3 100644 --- a/packages/yew/Cargo.toml +++ b/packages/yew/Cargo.toml @@ -32,6 +32,7 @@ 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"] } +wasm-bindgen-futures = "0.4" [dependencies.web-sys] version = "0.3" @@ -67,11 +68,6 @@ features = [ "HtmlScriptElement", ] - [target.'cfg(target_arch = "wasm32")'.dependencies] - # we move it here so no promise-based spawn_local can present for - # non-wasm32 targets. - wasm-bindgen-futures = "0.4" - [target.'cfg(not(target_arch = "wasm32"))'.dependencies] tokio = { version = "1.15.0", features = ["rt"], optional = true } From 92ebe1ac94ab081afb2b87ce85b92dce0ab9988d Mon Sep 17 00:00:00 2001 From: Kaede Hoshikawa Date: Mon, 23 May 2022 20:32:43 +0900 Subject: [PATCH 27/31] Second attempt to remove wasm-bindgen-futures. --- packages/yew/Cargo.toml | 6 +++++- .../hooks/use_prepared_state/feat_hydration.rs | 16 ++++++++++++---- 2 files changed, 17 insertions(+), 5 deletions(-) diff --git a/packages/yew/Cargo.toml b/packages/yew/Cargo.toml index 78872fb50b3..e200ac93687 100644 --- a/packages/yew/Cargo.toml +++ b/packages/yew/Cargo.toml @@ -32,7 +32,6 @@ 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"] } -wasm-bindgen-futures = "0.4" [dependencies.web-sys] version = "0.3" @@ -68,6 +67,11 @@ features = [ "HtmlScriptElement", ] +[target.'cfg(target_arch = "wasm32")'.dependencies] + # we move it here so no promise-based spawn_local can present for + # non-wasm32 targets. + wasm-bindgen-futures = "0.4" + [target.'cfg(not(target_arch = "wasm32"))'.dependencies] tokio = { version = "1.15.0", features = ["rt"], optional = true } diff --git a/packages/yew/src/functional/hooks/use_prepared_state/feat_hydration.rs b/packages/yew/src/functional/hooks/use_prepared_state/feat_hydration.rs index 94770b1b6e4..597c0c4c790 100644 --- a/packages/yew/src/functional/hooks/use_prepared_state/feat_hydration.rs +++ b/packages/yew/src/functional/hooks/use_prepared_state/feat_hydration.rs @@ -3,19 +3,22 @@ use std::marker::PhantomData; use std::rc::Rc; -use gloo_utils::window; -use js_sys::Uint8Array; use serde::de::DeserializeOwned; use serde::Serialize; -use wasm_bindgen::{JsCast, JsValue}; -use wasm_bindgen_futures::JsFuture; +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, 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) @@ -31,6 +34,11 @@ async fn decode_base64(s: &str) -> Result, JsValue> { Ok(content_array.to_vec()) } +#[cfg(not(target_arch = "wasm32"))] +async fn decode_base64(_s: &str) -> Result, JsValue> { + unreachable!("this function is not callable under non-wasm targets!"); +} + #[doc(hidden)] pub fn use_prepared_state(deps: D) -> impl Hook>>> where From 5784ec727c231a7d343c90acce4fded49e62b851 Mon Sep 17 00:00:00 2001 From: Kaede Hoshikawa Date: Mon, 23 May 2022 20:38:24 +0900 Subject: [PATCH 28/31] Remove spaces as well. --- packages/yew/Cargo.toml | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/packages/yew/Cargo.toml b/packages/yew/Cargo.toml index e200ac93687..1571d305790 100644 --- a/packages/yew/Cargo.toml +++ b/packages/yew/Cargo.toml @@ -68,9 +68,9 @@ features = [ ] [target.'cfg(target_arch = "wasm32")'.dependencies] - # we move it here so no promise-based spawn_local can present for - # non-wasm32 targets. - wasm-bindgen-futures = "0.4" +# we move it here so no promise-based spawn_local can present for +# non-wasm32 targets. +wasm-bindgen-futures = "0.4" [target.'cfg(not(target_arch = "wasm32"))'.dependencies] tokio = { version = "1.15.0", features = ["rt"], optional = true } From 7e12d6199a7d08fd13540a3134facb24facc05a4 Mon Sep 17 00:00:00 2001 From: Kaede Hoshikawa Date: Mon, 23 May 2022 22:06:46 +0900 Subject: [PATCH 29/31] Address reviews. --- .../hook_macro/use_prepared_state-fail.rs | 30 ++++++++++ .../hook_macro/use_prepared_state-fail.stderr | 55 +++++++++++++++++++ .../hook_macro/use_transitive_state-fail.rs | 26 +++++++++ .../use_transitive_state-fail.stderr | 39 +++++++++++++ packages/yew-macro/tests/hook_macro_test.rs | 7 +++ .../hooks/use_prepared_state/mod.rs | 10 ++-- .../hooks/use_transitive_state/mod.rs | 5 +- 7 files changed, 166 insertions(+), 6 deletions(-) create mode 100644 packages/yew-macro/tests/hook_macro/use_prepared_state-fail.rs create mode 100644 packages/yew-macro/tests/hook_macro/use_prepared_state-fail.stderr create mode 100644 packages/yew-macro/tests/hook_macro/use_transitive_state-fail.rs create mode 100644 packages/yew-macro/tests/hook_macro/use_transitive_state-fail.stderr create mode 100644 packages/yew-macro/tests/hook_macro_test.rs diff --git a/packages/yew-macro/tests/hook_macro/use_prepared_state-fail.rs b/packages/yew-macro/tests/hook_macro/use_prepared_state-fail.rs new file mode 100644 index 00000000000..e12ee98fe64 --- /dev/null +++ b/packages/yew-macro/tests/hook_macro/use_prepared_state-fail.rs @@ -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() {} diff --git a/packages/yew-macro/tests/hook_macro/use_prepared_state-fail.stderr b/packages/yew-macro/tests/hook_macro/use_prepared_state-fail.stderr new file mode 100644 index 00000000000..6c0cffa5740 --- /dev/null +++ b/packages/yew-macro/tests/hook_macro/use_prepared_state-fail.stderr @@ -0,0 +1,55 @@ +error: expected `|` + --> 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: expected `,` + --> 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: expected `,` + --> 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 `|` + --> 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: expected `,` + --> 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: expected `,` + --> 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) diff --git a/packages/yew-macro/tests/hook_macro/use_transitive_state-fail.rs b/packages/yew-macro/tests/hook_macro/use_transitive_state-fail.rs new file mode 100644 index 00000000000..113b0404ed3 --- /dev/null +++ b/packages/yew-macro/tests/hook_macro/use_transitive_state-fail.rs @@ -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() {} diff --git a/packages/yew-macro/tests/hook_macro/use_transitive_state-fail.stderr b/packages/yew-macro/tests/hook_macro/use_transitive_state-fail.stderr new file mode 100644 index 00000000000..7c3a14eddce --- /dev/null +++ b/packages/yew-macro/tests/hook_macro/use_transitive_state-fail.stderr @@ -0,0 +1,39 @@ +error: expected `|` + --> 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: expected `,` + --> 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 `|` + --> 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: expected `,` + --> 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) diff --git a/packages/yew-macro/tests/hook_macro_test.rs b/packages/yew-macro/tests/hook_macro_test.rs new file mode 100644 index 00000000000..d860ad28821 --- /dev/null +++ b/packages/yew-macro/tests/hook_macro_test.rs @@ -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"); +} diff --git a/packages/yew/src/functional/hooks/use_prepared_state/mod.rs b/packages/yew/src/functional/hooks/use_prepared_state/mod.rs index 20e3fd8e39e..c54f9de9f66 100644 --- a/packages/yew/src/functional/hooks/use_prepared_state/mod.rs +++ b/packages/yew/src/functional/hooks/use_prepared_state/mod.rs @@ -32,11 +32,12 @@ pub use feat_ssr::*; /// It has the following signature: /// /// ``` -/// # use yew::prelude::*; /// # use serde::de::DeserializeOwned; /// # use serde::Serialize; /// # use std::rc::Rc; -/// # use yew::suspense::SuspensionResult; +/// use yew::prelude::*; +/// use yew::suspense::SuspensionResult; +/// /// #[hook] /// pub fn use_prepared_state(f: F, deps: D) -> SuspensionResult>> /// where @@ -53,12 +54,13 @@ pub use feat_ssr::*; /// When accepting an async closure, it has the following signature: /// /// ``` -/// # use yew::prelude::*; /// # use serde::de::DeserializeOwned; /// # use serde::Serialize; -/// # use yew::suspense::SuspensionResult; /// # use std::rc::Rc; /// # use std::future::Future; +/// use yew::prelude::*; +/// use yew::suspense::SuspensionResult; +/// /// #[hook] /// pub fn use_prepared_state( /// f: F, diff --git a/packages/yew/src/functional/hooks/use_transitive_state/mod.rs b/packages/yew/src/functional/hooks/use_transitive_state/mod.rs index 0b2f3006158..f8ef487b5cb 100644 --- a/packages/yew/src/functional/hooks/use_transitive_state/mod.rs +++ b/packages/yew/src/functional/hooks/use_transitive_state/mod.rs @@ -32,11 +32,12 @@ pub use feat_ssr::*; /// It has the following function signature: /// /// ``` -/// # use yew::prelude::*; /// # use serde::de::DeserializeOwned; /// # use serde::Serialize; /// # use std::rc::Rc; -/// # use yew::suspense::SuspensionResult; +/// use yew::prelude::*; +/// use yew::suspense::SuspensionResult; +/// /// #[hook] /// pub fn use_transitive_state(f: F, deps: D) -> SuspensionResult>> /// where From b8267eff50638747c1683c0ca15206fa66208500 Mon Sep 17 00:00:00 2001 From: Kaede Hoshikawa Date: Mon, 23 May 2022 22:28:15 +0900 Subject: [PATCH 30/31] Better diagnostic message. --- packages/yew-macro/src/use_prepared_state.rs | 4 +++- packages/yew-macro/src/use_transitive_state.rs | 4 +++- .../tests/hook_macro/use_prepared_state-fail.stderr | 8 ++++---- .../tests/hook_macro/use_transitive_state-fail.stderr | 4 ++-- 4 files changed, 12 insertions(+), 8 deletions(-) diff --git a/packages/yew-macro/src/use_prepared_state.rs b/packages/yew-macro/src/use_prepared_state.rs index dda0f8fdade..771d765699c 100644 --- a/packages/yew-macro/src/use_prepared_state.rs +++ b/packages/yew-macro/src/use_prepared_state.rs @@ -15,7 +15,9 @@ impl Parse for PreparedState { // Reads a closure. let closure: ExprClosure = input.parse()?; - input.parse::()?; + input + .parse::() + .map_err(|e| syn::Error::new(e.span(), "expected a second argument as dependency"))?; let return_type = match &closure.output { ReturnType::Default => { diff --git a/packages/yew-macro/src/use_transitive_state.rs b/packages/yew-macro/src/use_transitive_state.rs index f7fde858c29..b3a6dbc2047 100644 --- a/packages/yew-macro/src/use_transitive_state.rs +++ b/packages/yew-macro/src/use_transitive_state.rs @@ -15,7 +15,9 @@ impl Parse for TransitiveState { // Reads a closure. let closure: ExprClosure = input.parse()?; - input.parse::()?; + input + .parse::() + .map_err(|e| syn::Error::new(e.span(), "expected a second argument as dependency"))?; let return_type = match &closure.output { ReturnType::Default => { diff --git a/packages/yew-macro/tests/hook_macro/use_prepared_state-fail.stderr b/packages/yew-macro/tests/hook_macro/use_prepared_state-fail.stderr index 6c0cffa5740..37206a3dcef 100644 --- a/packages/yew-macro/tests/hook_macro/use_prepared_state-fail.stderr +++ b/packages/yew-macro/tests/hook_macro/use_prepared_state-fail.stderr @@ -10,7 +10,7 @@ error: You must specify a return type for this closure. This is used when the cl 8 | use_prepared_state_with_closure!(|_| { todo!() }, 123)?; | ^^^^^^^^^^^^^^^ -error: expected `,` +error: expected a second argument as dependency --> tests/hook_macro/use_prepared_state-fail.rs:10:5 | 10 | use_prepared_state_with_closure!(|_| -> u32 { todo!() })?; @@ -18,7 +18,7 @@ error: expected `,` | = 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 `,` +error: expected a second argument as dependency --> tests/hook_macro/use_prepared_state-fail.rs:12:5 | 12 | use_prepared_state_with_closure!(async |_| -> u32 { todo!() })?; @@ -38,7 +38,7 @@ error: You must specify a return type for this closure. This is used when the cl 21 | use_prepared_state_without_closure!(|_| { todo!() }, 123)?; | ^^^^^^^^^^^^^^^ -error: expected `,` +error: expected a second argument as dependency --> tests/hook_macro/use_prepared_state-fail.rs:23:5 | 23 | use_prepared_state_without_closure!(|_| -> u32 { todo!() })?; @@ -46,7 +46,7 @@ error: expected `,` | = note: this error originates in the macro `use_prepared_state_without_closure` (in Nightly builds, run with -Z macro-backtrace for more info) -error: expected `,` +error: expected a second argument as dependency --> tests/hook_macro/use_prepared_state-fail.rs:25:5 | 25 | use_prepared_state_without_closure!(async |_| -> u32 { todo!() })?; diff --git a/packages/yew-macro/tests/hook_macro/use_transitive_state-fail.stderr b/packages/yew-macro/tests/hook_macro/use_transitive_state-fail.stderr index 7c3a14eddce..f824f06abb5 100644 --- a/packages/yew-macro/tests/hook_macro/use_transitive_state-fail.stderr +++ b/packages/yew-macro/tests/hook_macro/use_transitive_state-fail.stderr @@ -10,7 +10,7 @@ error: You must specify a return type for this closure. This is used when the cl 8 | use_transitive_state_with_closure!(|_| { todo!() }, 123)?; | ^^^^^^^^^^^^^^^ -error: expected `,` +error: expected a second argument as dependency --> tests/hook_macro/use_transitive_state-fail.rs:10:5 | 10 | use_transitive_state_with_closure!(|_| -> u32 { todo!() })?; @@ -30,7 +30,7 @@ error: You must specify a return type for this closure. This is used when the cl 19 | use_transitive_state_without_closure!(|_| { todo!() }, 123)?; | ^^^^^^^^^^^^^^^ -error: expected `,` +error: expected a second argument as dependency --> tests/hook_macro/use_transitive_state-fail.rs:21:5 | 21 | use_transitive_state_without_closure!(|_| -> u32 { todo!() })?; From f01c200334a8fb8bb2445bc869db6fbb0417c09a Mon Sep 17 00:00:00 2001 From: Kaede Hoshikawa Date: Tue, 24 May 2022 08:38:40 +0900 Subject: [PATCH 31/31] Update diagnostic messages. --- packages/yew-macro/src/use_prepared_state.rs | 16 ++++++++++++---- packages/yew-macro/src/use_transitive_state.rs | 16 ++++++++++++---- .../hook_macro/use_prepared_state-fail.stderr | 12 ++++++------ .../hook_macro/use_transitive_state-fail.stderr | 8 ++++---- 4 files changed, 34 insertions(+), 18 deletions(-) diff --git a/packages/yew-macro/src/use_prepared_state.rs b/packages/yew-macro/src/use_prepared_state.rs index 771d765699c..c94240aab1c 100644 --- a/packages/yew-macro/src/use_prepared_state.rs +++ b/packages/yew-macro/src/use_prepared_state.rs @@ -13,11 +13,19 @@ pub struct PreparedState { impl Parse for PreparedState { fn parse(input: ParseStream) -> syn::Result { // Reads a closure. - let closure: ExprClosure = input.parse()?; + let expr: Expr = input.parse()?; - input - .parse::() - .map_err(|e| syn::Error::new(e.span(), "expected a second argument as dependency"))?; + let closure = match expr { + Expr::Closure(m) => m, + other => return Err(syn::Error::new_spanned(other, "expected closure")), + }; + + input.parse::().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 => { diff --git a/packages/yew-macro/src/use_transitive_state.rs b/packages/yew-macro/src/use_transitive_state.rs index b3a6dbc2047..08bf9fc8265 100644 --- a/packages/yew-macro/src/use_transitive_state.rs +++ b/packages/yew-macro/src/use_transitive_state.rs @@ -13,11 +13,19 @@ pub struct TransitiveState { impl Parse for TransitiveState { fn parse(input: ParseStream) -> syn::Result { // Reads a closure. - let closure: ExprClosure = input.parse()?; + let expr: Expr = input.parse()?; - input - .parse::() - .map_err(|e| syn::Error::new(e.span(), "expected a second argument as dependency"))?; + let closure = match expr { + Expr::Closure(m) => m, + other => return Err(syn::Error::new_spanned(other, "expected closure")), + }; + + input.parse::().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 => { diff --git a/packages/yew-macro/tests/hook_macro/use_prepared_state-fail.stderr b/packages/yew-macro/tests/hook_macro/use_prepared_state-fail.stderr index 37206a3dcef..62ba0033a56 100644 --- a/packages/yew-macro/tests/hook_macro/use_prepared_state-fail.stderr +++ b/packages/yew-macro/tests/hook_macro/use_prepared_state-fail.stderr @@ -1,4 +1,4 @@ -error: expected `|` +error: expected closure --> tests/hook_macro/use_prepared_state-fail.rs:6:38 | 6 | use_prepared_state_with_closure!(123)?; @@ -10,7 +10,7 @@ error: You must specify a return type for this closure. This is used when the cl 8 | use_prepared_state_with_closure!(|_| { todo!() }, 123)?; | ^^^^^^^^^^^^^^^ -error: expected a second argument as dependency +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!() })?; @@ -18,7 +18,7 @@ error: expected a second argument as dependency | = 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 a second argument as dependency +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!() })?; @@ -26,7 +26,7 @@ error: expected a second argument as dependency | = 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 `|` +error: expected closure --> tests/hook_macro/use_prepared_state-fail.rs:19:41 | 19 | use_prepared_state_without_closure!(123)?; @@ -38,7 +38,7 @@ error: You must specify a return type for this closure. This is used when the cl 21 | use_prepared_state_without_closure!(|_| { todo!() }, 123)?; | ^^^^^^^^^^^^^^^ -error: expected a second argument as dependency +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!() })?; @@ -46,7 +46,7 @@ error: expected a second argument as dependency | = note: this error originates in the macro `use_prepared_state_without_closure` (in Nightly builds, run with -Z macro-backtrace for more info) -error: expected a second argument as dependency +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!() })?; diff --git a/packages/yew-macro/tests/hook_macro/use_transitive_state-fail.stderr b/packages/yew-macro/tests/hook_macro/use_transitive_state-fail.stderr index f824f06abb5..94712261373 100644 --- a/packages/yew-macro/tests/hook_macro/use_transitive_state-fail.stderr +++ b/packages/yew-macro/tests/hook_macro/use_transitive_state-fail.stderr @@ -1,4 +1,4 @@ -error: expected `|` +error: expected closure --> tests/hook_macro/use_transitive_state-fail.rs:6:40 | 6 | use_transitive_state_with_closure!(123)?; @@ -10,7 +10,7 @@ error: You must specify a return type for this closure. This is used when the cl 8 | use_transitive_state_with_closure!(|_| { todo!() }, 123)?; | ^^^^^^^^^^^^^^^ -error: expected a second argument as dependency +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!() })?; @@ -18,7 +18,7 @@ error: expected a second argument as dependency | = 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 `|` +error: expected closure --> tests/hook_macro/use_transitive_state-fail.rs:17:43 | 17 | use_transitive_state_without_closure!(123)?; @@ -30,7 +30,7 @@ error: You must specify a return type for this closure. This is used when the cl 19 | use_transitive_state_without_closure!(|_| { todo!() }, 123)?; | ^^^^^^^^^^^^^^^ -error: expected a second argument as dependency +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!() })?;