From c1ed876546e65c559e8a74bfbd6c603a2abc397a Mon Sep 17 00:00:00 2001 From: Kaede Hoshikawa Date: Wed, 12 Jan 2022 23:10:34 +0900 Subject: [PATCH 01/42] Make a use_hook hook with the new Hook trait. --- packages/yew-macro/src/hook.rs | 126 +++++++++++++++++++ packages/yew-macro/src/lib.rs | 23 +++- packages/yew/src/functional/hooks/mod.rs | 99 ++++++++++----- packages/yew/src/functional/hooks/use_ref.rs | 64 +++++++++- packages/yew/src/functional/mod.rs | 56 ++++++++- 5 files changed, 330 insertions(+), 38 deletions(-) create mode 100644 packages/yew-macro/src/hook.rs diff --git a/packages/yew-macro/src/hook.rs b/packages/yew-macro/src/hook.rs new file mode 100644 index 00000000000..f6a2c719030 --- /dev/null +++ b/packages/yew-macro/src/hook.rs @@ -0,0 +1,126 @@ +use proc_macro2::{Span, TokenStream}; +use proc_macro_error::emit_error; +use quote::ToTokens; +use quote::{quote, quote_spanned}; +use syn::parse::{Parse, ParseStream}; +use syn::spanned::Spanned; +use syn::{Expr, Ident, ItemFn, ReturnType, Signature, Stmt}; + +#[derive(Clone)] +pub struct HookFn { + inner: ItemFn, +} + +/// Creates lints on hooks in invalid position in an expression. +fn lint_expr(expr: Expr) -> TokenStream { + todo!() +} + +/// Rewrite valid hooks and lint invalid hooks in an expression. +/// +/// returns (branched, TokenStream) +fn rewrite_expr(expr: Expr) -> (bool, TokenStream) { + todo!() +} + +/// Creates lints on hooks in invalid position in a statement. +fn lint_stmt(stmt: Stmt) -> TokenStream { + todo!() +} + +/// Rewrite valid hooks in a statement and lint invalid hooks. +/// +/// returns (branched, TokenStream) +fn rewrite_stmt(stmt: Stmt) -> (bool, TokenStream) { + todo!() +} + +impl Parse for HookFn { + fn parse(input: ParseStream) -> syn::Result { + let func: ItemFn = input.parse()?; + + let sig = func.sig.clone(); + + if sig.asyncness.is_some() { + emit_error!(sig.asyncness, "hooks can't be async functions"); + } + + if sig.constness.is_some() { + emit_error!(sig.constness, "const functions can't be hooks"); + } + + if sig.abi.is_some() { + emit_error!(sig.abi, "extern functions can't be hooks"); + } + + if sig.unsafety.is_some() { + emit_error!(sig.unsafety, "unsafe functions can't be hooks"); + } + + if !sig.ident.to_string().starts_with("use_") { + emit_error!(sig.ident, "hooks must have a name starting with `use_`"); + } + + Ok(Self { inner: func }) + } +} + +fn rewrite_return_type(rt_type: &ReturnType) -> TokenStream { + match rt_type { + ReturnType::Default => quote! { -> impl ::yew::functional::Hook }, + ReturnType::Type(arrow, ref return_type) => { + quote_spanned! { return_type.span() => #arrow impl ::yew::functional::Hook } + } + } +} + +pub fn hook_impl(component: HookFn) -> syn::Result { + let HookFn { inner } = component; + + let ItemFn { + vis, + sig, + block, + attrs, + } = inner; + + let Signature { + fn_token, + ident, + generics, + inputs, + output: return_type, + .. + } = sig; + + let hook_return_type = rewrite_return_type(&return_type); + let output_type = match &return_type { + ReturnType::Default => quote! { () }, + ReturnType::Type(_, ref m) => m.clone().into_token_stream(), + }; + let stmts = block.stmts; + + let hook_struct_name = Ident::new("HookProvider", Span::mixed_site()); + + let (impl_generics, ty_generics, where_clause) = generics.split_for_impl(); + let states = Ident::new("states", Span::mixed_site()); + + let output = quote! { + #(#attrs)* + #vis #fn_token #ident #generics (#inputs) #hook_return_type { + struct #hook_struct_name #generics {} + + impl #impl_generics ::yew::functional::Hook for #hook_struct_name #ty_generics #where_clause { + type Output = #output_type; + + fn run(mut self, #states: &mut ::yew::functional::HookStates) -> Self::Output { + #(#stmts)* + } + } + + #hook_struct_name {} + } + }; + + Ok(output) +} diff --git a/packages/yew-macro/src/lib.rs b/packages/yew-macro/src/lib.rs index fe1d8940ae1..9efe3071c5e 100644 --- a/packages/yew-macro/src/lib.rs +++ b/packages/yew-macro/src/lib.rs @@ -49,12 +49,14 @@ mod classes; mod derive_props; mod function_component; +mod hook; mod html_tree; mod props; mod stringify; use derive_props::DerivePropsInput; use function_component::{function_component_impl, FunctionComponent, FunctionComponentName}; +use hook::{hook_impl, HookFn}; use html_tree::{HtmlRoot, HtmlRootVNode}; use proc_macro::TokenStream; use quote::ToTokens; @@ -123,10 +125,7 @@ pub fn classes(input: TokenStream) -> TokenStream { } #[proc_macro_attribute] -pub fn function_component( - attr: proc_macro::TokenStream, - item: proc_macro::TokenStream, -) -> proc_macro::TokenStream { +pub fn function_component(attr: TokenStream, item: TokenStream) -> proc_macro::TokenStream { let item = parse_macro_input!(item as FunctionComponent); let attr = parse_macro_input!(attr as FunctionComponentName); @@ -134,3 +133,19 @@ pub fn function_component( .unwrap_or_else(|err| err.to_compile_error()) .into() } + +#[proc_macro_error::proc_macro_error] +#[proc_macro_attribute] +pub fn hook(attr: TokenStream, item: TokenStream) -> proc_macro::TokenStream { + let item = parse_macro_input!(item as HookFn); + + if let Some(m) = proc_macro2::TokenStream::from(attr).into_iter().next() { + return syn::Error::new_spanned(m, "hook attribute does not accept any arguments") + .into_compile_error() + .into(); + } + + hook_impl(item) + .unwrap_or_else(|err| err.to_compile_error()) + .into() +} diff --git a/packages/yew/src/functional/hooks/mod.rs b/packages/yew/src/functional/hooks/mod.rs index 8b37ccb46dd..7f19118e3ec 100644 --- a/packages/yew/src/functional/hooks/mod.rs +++ b/packages/yew/src/functional/hooks/mod.rs @@ -10,10 +10,19 @@ pub use use_reducer::*; pub use use_ref::*; pub use use_state::*; -use crate::functional::{HookUpdater, CURRENT_HOOK}; -use std::cell::RefCell; -use std::ops::DerefMut; -use std::rc::Rc; +use crate::functional::{HookStates, HookUpdater, CURRENT_HOOK}; + +/// A trait that is implemented on hooks. +/// +/// A hook is usually defined via `#[hooks]`. Please refer to its documentation on how to implement +/// hooks. +pub trait Hook { + /// The return type when a hook is run. + type Output; + + /// Runs the hook inside current state, returns output upon completion. + fn run(self, states: &mut HookStates) -> Self::Output; +} /// Low level building block of creating hooks. /// @@ -38,31 +47,8 @@ pub fn use_hook= hook_state.hooks.len() { - let initial_state = Rc::new(RefCell::new(initializer())); - hook_state.hooks.push(initial_state.clone()); - hook_state.destroy_listeners.push(Box::new(move || { - destructor(initial_state.borrow_mut().deref_mut()); - })); - } - - let hook = hook_state - .hooks - .get(hook_pos) - .expect("Not the same number of hooks. Hooks must not be called conditionally") - .clone(); - - HookUpdater { - hook, - process_message: hook_state.process_message.clone(), - } - }); + let updater = CURRENT_HOOK + .with(|hook_state| hook_state.next_state::(initializer, destructor)); // Execute the actual hook closure we were given. Let it mutate the hook state and let // it create a callback that takes the mutable hook state. @@ -73,3 +59,58 @@ pub fn use_hook( + initializer: INIT, + runner: RUN, + destructor: TEAR, +) -> impl Hook +where + T: 'static, + INIT: 'static + FnOnce() -> T, + RUN: 'static + FnOnce(&mut T, HookUpdater) -> O, + TEAR: 'static + FnOnce(&mut T), +{ + struct HookProvider { + initializer: Box T>, + runner: Box O>, + destructor: Box, + } + + impl Hook for HookProvider + where + T: 'static, + { + type Output = O; + + fn run(self, states: &mut HookStates) -> Self::Output { + let Self { + initializer, + runner, + destructor, + } = self; + + // Extract current hook + let updater = states.next_state::(initializer, destructor); + + // Execute the actual hook closure we were given. Let it mutate the hook state and let + // it create a callback that takes the mutable hook state. + let mut hook = updater.hook.borrow_mut(); + let hook: &mut T = hook + .downcast_mut() + .expect("Incompatible hook type. Hooks must always be called in the same order"); + + runner(hook, updater.clone()) + } + } + + HookProvider { + initializer: Box::new(initializer), + runner: Box::new(runner), + destructor: Box::new(destructor), + } +} diff --git a/packages/yew/src/functional/hooks/use_ref.rs b/packages/yew/src/functional/hooks/use_ref.rs index d334a4bbee2..f041bc33403 100644 --- a/packages/yew/src/functional/hooks/use_ref.rs +++ b/packages/yew/src/functional/hooks/use_ref.rs @@ -88,7 +88,7 @@ pub fn use_ref(initial_value: impl FnOnce() -> T) -> Rc { /// /// { /// let div_ref = div_ref.clone(); -/// +/// /// use_effect_with_deps( /// |div_ref| { /// let div = div_ref @@ -128,3 +128,65 @@ pub fn use_ref(initial_value: impl FnOnce() -> T) -> Rc { pub fn use_node_ref() -> NodeRef { use_hook(NodeRef::default, |state, _| state.clone(), |_| {}) } + +/// This hook is used for obtaining a [`NodeRef`]. +/// It persists across renders. +/// +/// It is important to note that you do not get notified of state changes. +/// +/// # Example +/// ```rust +/// # use wasm_bindgen::{prelude::Closure, JsCast}; +/// # use yew::{ +/// # function_component, html, use_effect_with_deps, use_node_ref, +/// # Html, +/// # }; +/// # use web_sys::{Event, HtmlElement}; +/// +/// #[function_component(UseNodeRef)] +/// pub fn node_ref_hook() -> Html { +/// let div_ref = use_node_ref(); +/// +/// { +/// let div_ref = div_ref.clone(); +/// +/// use_effect_with_deps( +/// |div_ref| { +/// let div = div_ref +/// .cast::() +/// .expect("div_ref not attached to div element"); +/// +/// let listener = Closure::::wrap(Box::new(|_| { +/// web_sys::console::log_1(&"Clicked!".into()); +/// })); +/// +/// div.add_event_listener_with_callback( +/// "click", +/// listener.as_ref().unchecked_ref(), +/// ) +/// .unwrap(); +/// +/// move || { +/// div.remove_event_listener_with_callback( +/// "click", +/// listener.as_ref().unchecked_ref(), +/// ) +/// .unwrap(); +/// } +/// }, +/// div_ref, +/// ); +/// } +/// +/// html! { +///
+/// { "Click me and watch the console log!" } +///
+/// } +/// } +/// +/// ``` +#[crate::functional::hook] +pub fn use_node_ref_next() -> NodeRef { + use_hook(NodeRef::default, |state, _| state.clone(), |_| {}) +} diff --git a/packages/yew/src/functional/mod.rs b/packages/yew/src/functional/mod.rs index 5b23411f9e0..456218dac66 100644 --- a/packages/yew/src/functional/mod.rs +++ b/packages/yew/src/functional/mod.rs @@ -18,6 +18,7 @@ use crate::Properties; use scoped_tls_hkt::scoped_thread_local; use std::cell::RefCell; use std::fmt; +use std::ops::DerefMut; use std::rc::Rc; mod hooks; @@ -55,12 +56,16 @@ use crate::html::SealedBaseComponent; /// ``` pub use yew_macro::function_component; -scoped_thread_local!(static mut CURRENT_HOOK: HookState); +/// This attribute creates a hook from a normal Rust function. +pub use yew_macro::hook; + +scoped_thread_local!(static mut CURRENT_HOOK: HookStates); type Msg = Box bool>; type ProcessMessage = Rc; -struct HookState { +/// A hook state. +pub struct HookStates { counter: usize, scope: AnyScope, process_message: ProcessMessage, @@ -68,6 +73,49 @@ struct HookState { destroy_listeners: Vec>, } +impl HookStates { + pub(crate) fn next_state( + &mut self, + initializer: INIT, + destructor: TEAR, + ) -> HookUpdater + where + T: 'static, + INIT: FnOnce() -> T, + TEAR: FnOnce(&mut T) + 'static, + { + // Determine which hook position we're at and increment for the next hook + let hook_pos = self.counter; + self.counter += 1; + + // Initialize hook if this is the first call + if hook_pos >= self.hooks.len() { + let initial_state = Rc::new(RefCell::new(initializer())); + self.hooks.push(initial_state.clone()); + self.destroy_listeners.push(Box::new(move || { + destructor(initial_state.borrow_mut().deref_mut()); + })); + } + + let hook = self + .hooks + .get(hook_pos) + .expect("Not the same number of hooks. Hooks must not be called conditionally") + .clone(); + + HookUpdater { + hook, + process_message: self.process_message.clone(), + } + } +} + +impl fmt::Debug for HookStates { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + f.write_str("HookStates<_>") + } +} + /// Trait that allows a struct to act as Function Component. pub trait FunctionProvider { /// Properties for the Function Component. @@ -82,7 +130,7 @@ pub trait FunctionProvider { /// Wrapper that allows a struct implementing [`FunctionProvider`] to be consumed as a component. pub struct FunctionComponent { _never: std::marker::PhantomData, - hook_state: RefCell, + hook_state: RefCell, message_queue: MsgQueue, } @@ -117,7 +165,7 @@ where Self { _never: std::marker::PhantomData::default(), message_queue: message_queue.clone(), - hook_state: RefCell::new(HookState { + hook_state: RefCell::new(HookStates { counter: 0, scope, process_message: { From 22d91f63358adad5a8dd938e69fba99e433a9914 Mon Sep 17 00:00:00 2001 From: Kaede Hoshikawa Date: Thu, 13 Jan 2022 20:41:30 +0900 Subject: [PATCH 02/42] Implement Lifetime. --- packages/yew-macro/Cargo.toml | 3 +- packages/yew-macro/src/hook/lifetime.rs | 84 +++++++++++++++++++ .../yew-macro/src/{hook.rs => hook/mod.rs} | 21 ++++- packages/yew/src/functional/hooks/mod.rs | 17 ++-- 4 files changed, 115 insertions(+), 10 deletions(-) create mode 100644 packages/yew-macro/src/hook/lifetime.rs rename packages/yew-macro/src/{hook.rs => hook/mod.rs} (90%) diff --git a/packages/yew-macro/Cargo.toml b/packages/yew-macro/Cargo.toml index 832a0fff3ab..c3b41a2e2f9 100644 --- a/packages/yew-macro/Cargo.toml +++ b/packages/yew-macro/Cargo.toml @@ -21,7 +21,8 @@ lazy_static = "1" proc-macro-error = "1" proc-macro2 = "1" quote = "1" -syn = { version = "1", features = ["full", "extra-traits"] } +syn = { version = "1", features = ["full", "extra-traits", "visit-mut"] } +once_cell = "1" # testing [dev-dependencies] diff --git a/packages/yew-macro/src/hook/lifetime.rs b/packages/yew-macro/src/hook/lifetime.rs new file mode 100644 index 00000000000..f91410c5b04 --- /dev/null +++ b/packages/yew-macro/src/hook/lifetime.rs @@ -0,0 +1,84 @@ +use proc_macro2::Span; +use syn::visit_mut::{self, VisitMut}; +use syn::{GenericArgument, Lifetime, Receiver, TypeReference}; + +/// Finds an unused lifetime. +pub fn find_available_lifetime(lifetimes: &CollectLifetimes) -> Lifetime { + for i in 0.. { + let hook_lifetime = if i == 0 { + Lifetime::new("'hook", lifetimes.default_span) + } else { + Lifetime::new(&format!("'hook{}", i), lifetimes.default_span) + }; + + if !lifetimes.elided.contains(&hook_lifetime) + && !lifetimes.explicit.contains(&hook_lifetime) + { + return hook_lifetime; + } + } + + unreachable!() +} + +// borrowed from the async-trait crate. +pub struct CollectLifetimes { + pub elided: Vec, + pub explicit: Vec, + pub name: &'static str, + pub default_span: Span, +} + +impl CollectLifetimes { + pub fn new(name: &'static str, default_span: Span) -> Self { + CollectLifetimes { + elided: Vec::new(), + explicit: Vec::new(), + name, + default_span, + } + } + + fn visit_opt_lifetime(&mut self, lifetime: &mut Option) { + match lifetime { + None => *lifetime = Some(self.next_lifetime(None)), + Some(lifetime) => self.visit_lifetime(lifetime), + } + } + + fn visit_lifetime(&mut self, lifetime: &mut Lifetime) { + if lifetime.ident == "_" { + *lifetime = self.next_lifetime(lifetime.span()); + } else { + self.explicit.push(lifetime.clone()); + } + } + + fn next_lifetime>>(&mut self, span: S) -> Lifetime { + let name = format!("{}{}", self.name, self.elided.len()); + let span = span.into().unwrap_or(self.default_span); + let life = Lifetime::new(&name, span); + self.elided.push(life.clone()); + life + } +} + +impl VisitMut for CollectLifetimes { + fn visit_receiver_mut(&mut self, arg: &mut Receiver) { + if let Some((_, lifetime)) = &mut arg.reference { + self.visit_opt_lifetime(lifetime); + } + } + + fn visit_type_reference_mut(&mut self, ty: &mut TypeReference) { + self.visit_opt_lifetime(&mut ty.lifetime); + visit_mut::visit_type_reference_mut(self, ty); + } + + fn visit_generic_argument_mut(&mut self, gen: &mut GenericArgument) { + if let GenericArgument::Lifetime(lifetime) = gen { + self.visit_lifetime(lifetime); + } + visit_mut::visit_generic_argument_mut(self, gen); + } +} diff --git a/packages/yew-macro/src/hook.rs b/packages/yew-macro/src/hook/mod.rs similarity index 90% rename from packages/yew-macro/src/hook.rs rename to packages/yew-macro/src/hook/mod.rs index f6a2c719030..0c77a403619 100644 --- a/packages/yew-macro/src/hook.rs +++ b/packages/yew-macro/src/hook/mod.rs @@ -4,7 +4,9 @@ use quote::ToTokens; use quote::{quote, quote_spanned}; use syn::parse::{Parse, ParseStream}; use syn::spanned::Spanned; -use syn::{Expr, Ident, ItemFn, ReturnType, Signature, Stmt}; +use syn::{Expr, Ident, ItemFn, LitStr, ReturnType, Signature, Stmt}; + +mod lifetime; #[derive(Clone)] pub struct HookFn { @@ -77,6 +79,22 @@ fn rewrite_return_type(rt_type: &ReturnType) -> TokenStream { pub fn hook_impl(component: HookFn) -> syn::Result { let HookFn { inner } = component; + let doc_text = LitStr::new( + &format!( + r#" +# Note + +When used in function components and hooks, this hook is equivalent to: + +``` +{} +``` +"#, + inner.sig.to_token_stream().to_string() + ), + Span::mixed_site(), + ); + let ItemFn { vis, sig, @@ -107,6 +125,7 @@ pub fn hook_impl(component: HookFn) -> syn::Result { let output = quote! { #(#attrs)* + #[doc = #doc_text] #vis #fn_token #ident #generics (#inputs) #hook_return_type { struct #hook_struct_name #generics {} diff --git a/packages/yew/src/functional/hooks/mod.rs b/packages/yew/src/functional/hooks/mod.rs index 7f19118e3ec..e22f85b889d 100644 --- a/packages/yew/src/functional/hooks/mod.rs +++ b/packages/yew/src/functional/hooks/mod.rs @@ -64,24 +64,25 @@ pub fn use_hook( +pub(crate) fn use_hook_next<'hook, T, INIT, RUN, TEAR, O>( initializer: INIT, runner: RUN, destructor: TEAR, -) -> impl Hook +) -> impl 'hook + Hook where T: 'static, - INIT: 'static + FnOnce() -> T, - RUN: 'static + FnOnce(&mut T, HookUpdater) -> O, + O: 'hook, + INIT: 'hook + FnOnce() -> T, + RUN: 'hook + FnOnce(&mut T, HookUpdater) -> O, TEAR: 'static + FnOnce(&mut T), { - struct HookProvider { - initializer: Box T>, - runner: Box O>, + struct HookProvider<'a, T, O> { + initializer: Box T + 'a>, + runner: Box O + 'a>, destructor: Box, } - impl Hook for HookProvider + impl Hook for HookProvider<'_, T, O> where T: 'static, { From d940a7da27b6db36be2b46e548040216401c9cf7 Mon Sep 17 00:00:00 2001 From: Kaede Hoshikawa Date: Thu, 13 Jan 2022 22:59:58 +0900 Subject: [PATCH 03/42] Rewrites function signature. --- packages/yew-macro/src/hook/body.rs | 24 +++++++ packages/yew-macro/src/hook/mod.rs | 105 +++++++++++++++++----------- 2 files changed, 90 insertions(+), 39 deletions(-) create mode 100644 packages/yew-macro/src/hook/body.rs diff --git a/packages/yew-macro/src/hook/body.rs b/packages/yew-macro/src/hook/body.rs new file mode 100644 index 00000000000..cda01e22757 --- /dev/null +++ b/packages/yew-macro/src/hook/body.rs @@ -0,0 +1,24 @@ +use syn::visit_mut::VisitMut; + +#[derive(Debug, Default)] +pub struct BodyRewriter { + branch_ctr: u64, +} + +impl BodyRewriter { + fn is_branched(&self) -> bool { + self.branch_ctr > 0 + } + + fn with_branch(&mut self, f: F) -> O + where + F: FnOnce() -> O, + { + self.branch_ctr += 1; + let result = f(); + self.branch_ctr -= 1; + result + } +} + +impl VisitMut for BodyRewriter {} diff --git a/packages/yew-macro/src/hook/mod.rs b/packages/yew-macro/src/hook/mod.rs index 0c77a403619..181f34218b1 100644 --- a/packages/yew-macro/src/hook/mod.rs +++ b/packages/yew-macro/src/hook/mod.rs @@ -4,8 +4,11 @@ use quote::ToTokens; use quote::{quote, quote_spanned}; use syn::parse::{Parse, ParseStream}; use syn::spanned::Spanned; -use syn::{Expr, Ident, ItemFn, LitStr, ReturnType, Signature, Stmt}; +use syn::token; +use syn::visit_mut; +use syn::{parse_quote, Ident, ItemFn, Lifetime, LitStr, ReturnType, Signature, WhereClause}; +mod body; mod lifetime; #[derive(Clone)] @@ -13,30 +16,6 @@ pub struct HookFn { inner: ItemFn, } -/// Creates lints on hooks in invalid position in an expression. -fn lint_expr(expr: Expr) -> TokenStream { - todo!() -} - -/// Rewrite valid hooks and lint invalid hooks in an expression. -/// -/// returns (branched, TokenStream) -fn rewrite_expr(expr: Expr) -> (bool, TokenStream) { - todo!() -} - -/// Creates lints on hooks in invalid position in a statement. -fn lint_stmt(stmt: Stmt) -> TokenStream { - todo!() -} - -/// Rewrite valid hooks in a statement and lint invalid hooks. -/// -/// returns (branched, TokenStream) -fn rewrite_stmt(stmt: Stmt) -> (bool, TokenStream) { - todo!() -} - impl Parse for HookFn { fn parse(input: ParseStream) -> syn::Result { let func: ItemFn = input.parse()?; @@ -67,11 +46,13 @@ impl Parse for HookFn { } } -fn rewrite_return_type(rt_type: &ReturnType) -> TokenStream { +fn rewrite_return_type(hook_lifetime: &Lifetime, rt_type: &ReturnType) -> TokenStream { match rt_type { - ReturnType::Default => quote! { -> impl ::yew::functional::Hook }, + ReturnType::Default => { + quote! { -> impl #hook_lifetime + ::yew::functional::Hook } + } ReturnType::Type(arrow, ref return_type) => { - quote_spanned! { return_type.span() => #arrow impl ::yew::functional::Hook } + quote_spanned! { return_type.span() => #arrow impl #hook_lifetime + ::yew::functional::Hook } } } } @@ -97,47 +78,93 @@ When used in function components and hooks, this hook is equivalent to: let ItemFn { vis, - sig, - block, + mut sig, + mut block, attrs, } = inner; let Signature { fn_token, ident, - generics, + mut generics, inputs, output: return_type, .. - } = sig; + } = sig.clone(); + + let mut lifetimes = lifetime::CollectLifetimes::new("'arg", ident.span()); + visit_mut::visit_signature_mut(&mut lifetimes, &mut sig); + + let hook_lifetime = lifetime::find_available_lifetime(&lifetimes); + + generics.params = { + let elided_lifetimes = &lifetimes.elided; + let params = generics.params; - let hook_return_type = rewrite_return_type(&return_type); + parse_quote!(#hook_lifetime, #(#elided_lifetimes,)* #params) + }; + + if !lifetimes.elided.is_empty() { + let mut where_clause = generics + .where_clause + .clone() + .unwrap_or_else(|| WhereClause { + where_token: token::Where { + span: Span::mixed_site(), + }, + predicates: Default::default(), + }); + + for elided in lifetimes.elided.iter() { + where_clause + .predicates + .push(parse_quote!(#elided: #hook_lifetime)); + } + + generics.where_clause = Some(where_clause); + } + + let hook_return_type = rewrite_return_type(&hook_lifetime, &return_type); let output_type = match &return_type { ReturnType::Default => quote! { () }, ReturnType::Type(_, ref m) => m.clone().into_token_stream(), }; - let stmts = block.stmts; let hook_struct_name = Ident::new("HookProvider", Span::mixed_site()); let (impl_generics, ty_generics, where_clause) = generics.split_for_impl(); let states = Ident::new("states", Span::mixed_site()); + let phantom_types = generics + .type_params() + .map(|ty_param| ty_param.ident.clone()) + .collect::>(); + + let phantom_lifetimes = generics + .lifetimes() + .map(|life| quote! { &#life () }) + .collect::>(); + + let mut body_rewriter = body::BodyRewriter::default(); + visit_mut::visit_block_mut(&mut body_rewriter, &mut *block); + let output = quote! { #(#attrs)* #[doc = #doc_text] #vis #fn_token #ident #generics (#inputs) #hook_return_type { - struct #hook_struct_name #generics {} + struct #hook_struct_name #generics { + _marker: ::std::marker::PhantomData<( #(#phantom_types,)* #(#phantom_lifetimes,)* )>, + } impl #impl_generics ::yew::functional::Hook for #hook_struct_name #ty_generics #where_clause { type Output = #output_type; - fn run(mut self, #states: &mut ::yew::functional::HookStates) -> Self::Output { - #(#stmts)* - } + fn run(mut self, #states: &mut ::yew::functional::HookStates) -> Self::Output #block } - #hook_struct_name {} + #hook_struct_name { + _marker: ::std::marker::PhantomData, + } } }; From 572becbd55771780175f43314e9b16e056dd4059 Mon Sep 17 00:00:00 2001 From: Kaede Hoshikawa Date: Sun, 16 Jan 2022 10:51:26 +0900 Subject: [PATCH 04/42] Only apply lifetime if there're other lifetimes. --- packages/yew-macro/src/hook/mod.rs | 33 +++++++++++++++++------------- 1 file changed, 19 insertions(+), 14 deletions(-) diff --git a/packages/yew-macro/src/hook/mod.rs b/packages/yew-macro/src/hook/mod.rs index 181f34218b1..ebb483caa06 100644 --- a/packages/yew-macro/src/hook/mod.rs +++ b/packages/yew-macro/src/hook/mod.rs @@ -46,13 +46,15 @@ impl Parse for HookFn { } } -fn rewrite_return_type(hook_lifetime: &Lifetime, rt_type: &ReturnType) -> TokenStream { +fn rewrite_return_type(hook_lifetime: Option<&Lifetime>, rt_type: &ReturnType) -> TokenStream { + let bound = hook_lifetime.map(|m| quote! { #m + }); + match rt_type { ReturnType::Default => { - quote! { -> impl #hook_lifetime + ::yew::functional::Hook } + quote! { -> impl #bound ::yew::functional::Hook } } ReturnType::Type(arrow, ref return_type) => { - quote_spanned! { return_type.span() => #arrow impl #hook_lifetime + ::yew::functional::Hook } + quote_spanned! { return_type.span() => #arrow impl #bound ::yew::functional::Hook } } } } @@ -71,7 +73,7 @@ When used in function components and hooks, this hook is equivalent to: {} ``` "#, - inner.sig.to_token_stream().to_string() + inner.sig.to_token_stream() ), Span::mixed_site(), ); @@ -95,16 +97,15 @@ When used in function components and hooks, this hook is equivalent to: let mut lifetimes = lifetime::CollectLifetimes::new("'arg", ident.span()); visit_mut::visit_signature_mut(&mut lifetimes, &mut sig); - let hook_lifetime = lifetime::find_available_lifetime(&lifetimes); - - generics.params = { - let elided_lifetimes = &lifetimes.elided; - let params = generics.params; + let hook_lifetime = if !lifetimes.elided.is_empty() { + let hook_lifetime = lifetime::find_available_lifetime(&lifetimes); + generics.params = { + let elided_lifetimes = &lifetimes.elided; + let params = generics.params; - parse_quote!(#hook_lifetime, #(#elided_lifetimes,)* #params) - }; + parse_quote!(#hook_lifetime, #(#elided_lifetimes,)* #params) + }; - if !lifetimes.elided.is_empty() { let mut where_clause = generics .where_clause .clone() @@ -122,9 +123,13 @@ When used in function components and hooks, this hook is equivalent to: } generics.where_clause = Some(where_clause); - } - let hook_return_type = rewrite_return_type(&hook_lifetime, &return_type); + Some(hook_lifetime) + } else { + None + }; + + let hook_return_type = rewrite_return_type(hook_lifetime.as_ref(), &return_type); let output_type = match &return_type { ReturnType::Default => quote! { () }, ReturnType::Type(_, ref m) => m.clone().into_token_stream(), From 7e8bcdd5262fbaf4eea10e9199844e235e74b5b3 Mon Sep 17 00:00:00 2001 From: Kaede Hoshikawa Date: Sat, 22 Jan 2022 13:58:40 +0900 Subject: [PATCH 05/42] Cleanup signature rewrite logic. --- packages/yew-macro/src/hook/mod.rs | 91 ++++-------------- packages/yew-macro/src/hook/signature.rs | 113 +++++++++++++++++++++++ 2 files changed, 130 insertions(+), 74 deletions(-) create mode 100644 packages/yew-macro/src/hook/signature.rs diff --git a/packages/yew-macro/src/hook/mod.rs b/packages/yew-macro/src/hook/mod.rs index ebb483caa06..58b63ad452c 100644 --- a/packages/yew-macro/src/hook/mod.rs +++ b/packages/yew-macro/src/hook/mod.rs @@ -1,15 +1,16 @@ use proc_macro2::{Span, TokenStream}; use proc_macro_error::emit_error; +use quote::quote; use quote::ToTokens; -use quote::{quote, quote_spanned}; use syn::parse::{Parse, ParseStream}; -use syn::spanned::Spanned; -use syn::token; use syn::visit_mut; -use syn::{parse_quote, Ident, ItemFn, Lifetime, LitStr, ReturnType, Signature, WhereClause}; +use syn::{Ident, ItemFn, LitStr, Signature}; mod body; mod lifetime; +mod signature; + +use signature::HookSignature; #[derive(Clone)] pub struct HookFn { @@ -46,19 +47,6 @@ impl Parse for HookFn { } } -fn rewrite_return_type(hook_lifetime: Option<&Lifetime>, rt_type: &ReturnType) -> TokenStream { - let bound = hook_lifetime.map(|m| quote! { #m + }); - - match rt_type { - ReturnType::Default => { - quote! { -> impl #bound ::yew::functional::Hook } - } - ReturnType::Type(arrow, ref return_type) => { - quote_spanned! { return_type.span() => #arrow impl #bound ::yew::functional::Hook } - } - } -} - pub fn hook_impl(component: HookFn) -> syn::Result { let HookFn { inner } = component; @@ -80,75 +68,30 @@ When used in function components and hooks, this hook is equivalent to: let ItemFn { vis, - mut sig, + sig, mut block, attrs, } = inner; + let hook_sig = HookSignature::rewrite(&sig); + let Signature { - fn_token, - ident, - mut generics, - inputs, - output: return_type, + ref fn_token, + ref ident, + ref inputs, + output: ref hook_return_type, + ref generics, .. - } = sig.clone(); - - let mut lifetimes = lifetime::CollectLifetimes::new("'arg", ident.span()); - visit_mut::visit_signature_mut(&mut lifetimes, &mut sig); - - let hook_lifetime = if !lifetimes.elided.is_empty() { - let hook_lifetime = lifetime::find_available_lifetime(&lifetimes); - generics.params = { - let elided_lifetimes = &lifetimes.elided; - let params = generics.params; - - parse_quote!(#hook_lifetime, #(#elided_lifetimes,)* #params) - }; - - let mut where_clause = generics - .where_clause - .clone() - .unwrap_or_else(|| WhereClause { - where_token: token::Where { - span: Span::mixed_site(), - }, - predicates: Default::default(), - }); - - for elided in lifetimes.elided.iter() { - where_clause - .predicates - .push(parse_quote!(#elided: #hook_lifetime)); - } - - generics.where_clause = Some(where_clause); - - Some(hook_lifetime) - } else { - None - }; - - let hook_return_type = rewrite_return_type(hook_lifetime.as_ref(), &return_type); - let output_type = match &return_type { - ReturnType::Default => quote! { () }, - ReturnType::Type(_, ref m) => m.clone().into_token_stream(), - }; + } = hook_sig.sig; + let output_type = &hook_sig.output_type; let hook_struct_name = Ident::new("HookProvider", Span::mixed_site()); let (impl_generics, ty_generics, where_clause) = generics.split_for_impl(); let states = Ident::new("states", Span::mixed_site()); - let phantom_types = generics - .type_params() - .map(|ty_param| ty_param.ident.clone()) - .collect::>(); - - let phantom_lifetimes = generics - .lifetimes() - .map(|life| quote! { &#life () }) - .collect::>(); + let phantom_types = hook_sig.phantom_types(); + let phantom_lifetimes = hook_sig.phantom_lifetimes(); let mut body_rewriter = body::BodyRewriter::default(); visit_mut::visit_block_mut(&mut body_rewriter, &mut *block); diff --git a/packages/yew-macro/src/hook/signature.rs b/packages/yew-macro/src/hook/signature.rs new file mode 100644 index 00000000000..9cbe6e434d5 --- /dev/null +++ b/packages/yew-macro/src/hook/signature.rs @@ -0,0 +1,113 @@ +use proc_macro2::Span; +use quote::quote; +use syn::spanned::Spanned; +use syn::{ + parse_quote, parse_quote_spanned, token, visit_mut, Ident, Lifetime, ReturnType, Signature, + Type, TypeReference, WhereClause, +}; + +use super::lifetime; + +pub struct HookSignature { + pub hook_lifetime: Option, + pub sig: Signature, + pub output_type: Type, +} + +impl HookSignature { + fn rewrite_return_type( + hook_lifetime: Option<&Lifetime>, + rt_type: &ReturnType, + ) -> (ReturnType, Type) { + let bound = hook_lifetime.map(|m| quote! { #m + }); + + match rt_type { + ReturnType::Default => ( + parse_quote! { -> impl #bound ::yew::functional::Hook }, + parse_quote! { () }, + ), + ReturnType::Type(arrow, ref return_type) => ( + parse_quote_spanned! { + return_type.span() => #arrow impl #bound ::yew::functional::Hook + }, + *return_type.clone(), + ), + } + } + + /// Rewrites a Hook Signature and extracts information. + pub fn rewrite(sig: &Signature) -> Self { + let mut sig = sig.clone(); + + let mut lifetimes = lifetime::CollectLifetimes::new("'arg", sig.ident.span()); + visit_mut::visit_signature_mut(&mut lifetimes, &mut sig); + + let Signature { + ref mut generics, + output: ref return_type, + .. + } = sig; + + let hook_lifetime = if !lifetimes.elided.is_empty() { + let hook_lifetime = lifetime::find_available_lifetime(&lifetimes); + generics.params = { + let elided_lifetimes = &lifetimes.elided; + let params = &generics.params; + + parse_quote!(#hook_lifetime, #(#elided_lifetimes,)* #params) + }; + + let mut where_clause = generics + .where_clause + .clone() + .unwrap_or_else(|| WhereClause { + where_token: token::Where { + span: Span::mixed_site(), + }, + predicates: Default::default(), + }); + + for elided in lifetimes.elided.iter() { + where_clause + .predicates + .push(parse_quote!(#elided: #hook_lifetime)); + } + + generics.where_clause = Some(where_clause); + + Some(hook_lifetime) + } else { + None + }; + + let (output, output_type) = Self::rewrite_return_type(hook_lifetime.as_ref(), return_type); + sig.output = output; + + Self { + hook_lifetime, + sig, + output_type, + } + } + + pub fn phantom_types(&self) -> Vec { + self.sig + .generics + .type_params() + .map(|ty_param| ty_param.ident.clone()) + .collect() + + // let phantom_lifetimes = generics + // .lifetimes() + // .map(|life| quote! { &#life () }) + // .collect::>(); + } + + pub fn phantom_lifetimes(&self) -> Vec { + self.sig + .generics + .lifetimes() + .map(|life| parse_quote! { &#life () }) + .collect() + } +} From 266414177e09bfa2d72ad60568dfe83d04dba8f0 Mon Sep 17 00:00:00 2001 From: Kaede Hoshikawa Date: Sat, 22 Jan 2022 14:59:13 +0900 Subject: [PATCH 06/42] Rewrite hook body. --- packages/yew-macro/src/hook/body.rs | 95 +++++++++++++++++++- packages/yew-macro/src/hook/mod.rs | 14 ++- packages/yew-macro/src/hook/signature.rs | 5 -- packages/yew/src/functional/hooks/use_ref.rs | 4 +- 4 files changed, 107 insertions(+), 11 deletions(-) diff --git a/packages/yew-macro/src/hook/body.rs b/packages/yew-macro/src/hook/body.rs index cda01e22757..29cf4d520f7 100644 --- a/packages/yew-macro/src/hook/body.rs +++ b/packages/yew-macro/src/hook/body.rs @@ -1,4 +1,11 @@ +use proc_macro2::Span; +use proc_macro_error::emit_error; +use syn::spanned::Spanned; use syn::visit_mut::VisitMut; +use syn::{ + parse_quote_spanned, visit_mut, Expr, ExprCall, ExprClosure, ExprForLoop, ExprIf, ExprLoop, + ExprMatch, ExprWhile, Ident, +}; #[derive(Debug, Default)] pub struct BodyRewriter { @@ -12,13 +19,95 @@ impl BodyRewriter { fn with_branch(&mut self, f: F) -> O where - F: FnOnce() -> O, + F: FnOnce(&mut BodyRewriter) -> O, { self.branch_ctr += 1; - let result = f(); + let result = { f(self) }; self.branch_ctr -= 1; result } } -impl VisitMut for BodyRewriter {} +impl VisitMut for BodyRewriter { + fn visit_expr_call_mut(&mut self, i: &mut ExprCall) { + let states_ident = Ident::new("states", Span::mixed_site()); + + // Only rewrite hook calls. + if let Expr::Path(ref m) = &*i.func { + if let Some(m) = m.path.segments.last().as_ref().map(|m| &m.ident) { + if m.to_string().starts_with("use_") { + if self.is_branched() { + emit_error!(i, "hooks cannot be called at this position."); + } else { + *i = parse_quote_spanned! { i.span() => ::yew::functional::Hook::run(#i, #states_ident) }; + } + + return; + } + } + } + + visit_mut::visit_expr_call_mut(self, i); + } + + fn visit_expr_closure_mut(&mut self, i: &mut ExprClosure) { + self.with_branch(move |m| visit_mut::visit_expr_closure_mut(m, i)) + } + + fn visit_expr_if_mut(&mut self, i: &mut ExprIf) { + for it in &mut i.attrs { + visit_mut::visit_attribute_mut(self, it); + } + + visit_mut::visit_expr_mut(self, &mut *i.cond); + + self.with_branch(|m| visit_mut::visit_block_mut(m, &mut i.then_branch)); + + if let Some(it) = &mut i.else_branch { + self.with_branch(|m| visit_mut::visit_expr_mut(m, &mut *(it).1)); + } + } + + fn visit_expr_loop_mut(&mut self, i: &mut ExprLoop) { + self.with_branch(|m| visit_mut::visit_expr_loop_mut(m, i)); + } + + fn visit_expr_for_loop_mut(&mut self, i: &mut ExprForLoop) { + for it in &mut i.attrs { + visit_mut::visit_attribute_mut(self, it); + } + if let Some(it) = &mut i.label { + visit_mut::visit_label_mut(self, it); + } + visit_mut::visit_pat_mut(self, &mut i.pat); + visit_mut::visit_expr_mut(self, &mut *i.expr); + + self.with_branch(|m| visit_mut::visit_block_mut(m, &mut i.body)); + } + + fn visit_expr_match_mut(&mut self, i: &mut ExprMatch) { + for it in &mut i.attrs { + visit_mut::visit_attribute_mut(self, it); + } + + visit_mut::visit_expr_mut(self, &mut *i.expr); + + self.with_branch(|m| { + for it in &mut i.arms { + visit_mut::visit_arm_mut(m, it); + } + }); + } + + fn visit_expr_while_mut(&mut self, i: &mut ExprWhile) { + for it in &mut i.attrs { + visit_mut::visit_attribute_mut(self, it); + } + if let Some(it) = &mut i.label { + visit_mut::visit_label_mut(self, it); + } + + self.with_branch(|m| visit_mut::visit_expr_mut(m, &mut i.cond)); + self.with_branch(|m| visit_mut::visit_block_mut(m, &mut i.body)); + } +} diff --git a/packages/yew-macro/src/hook/mod.rs b/packages/yew-macro/src/hook/mod.rs index 58b63ad452c..cb4f731125e 100644 --- a/packages/yew-macro/src/hook/mod.rs +++ b/packages/yew-macro/src/hook/mod.rs @@ -84,6 +84,7 @@ When used in function components and hooks, this hook is equivalent to: .. } = hook_sig.sig; + let hook_lifetime = hook_sig.hook_lifetime.as_ref(); let output_type = &hook_sig.output_type; let hook_struct_name = Ident::new("HookProvider", Span::mixed_site()); @@ -96,22 +97,33 @@ When used in function components and hooks, this hook is equivalent to: let mut body_rewriter = body::BodyRewriter::default(); visit_mut::visit_block_mut(&mut body_rewriter, &mut *block); + let hook_lifetime_plus = hook_lifetime.map(|m| quote! { #m + }); + let output = quote! { #(#attrs)* #[doc = #doc_text] #vis #fn_token #ident #generics (#inputs) #hook_return_type { + // always capture inputs with closure for now, we need boxing implementation for `impl Trait` + // arguments anyways. + let inner = ::std::boxed::Box::new(move |#states: &mut ::yew::functional::HookStates| #block) + as ::std::boxed::Box<#hook_lifetime_plus FnOnce(&mut ::yew::functional::HookStates) -> #output_type>; + struct #hook_struct_name #generics { _marker: ::std::marker::PhantomData<( #(#phantom_types,)* #(#phantom_lifetimes,)* )>, + inner: ::std::boxed::Box<#hook_lifetime_plus FnOnce(&mut ::yew::functional::HookStates) -> #output_type> } impl #impl_generics ::yew::functional::Hook for #hook_struct_name #ty_generics #where_clause { type Output = #output_type; - fn run(mut self, #states: &mut ::yew::functional::HookStates) -> Self::Output #block + fn run(mut self, #states: &mut ::yew::functional::HookStates) -> Self::Output { + (self.inner)(#states) + } } #hook_struct_name { _marker: ::std::marker::PhantomData, + inner, } } }; diff --git a/packages/yew-macro/src/hook/signature.rs b/packages/yew-macro/src/hook/signature.rs index 9cbe6e434d5..f1646ec8ed0 100644 --- a/packages/yew-macro/src/hook/signature.rs +++ b/packages/yew-macro/src/hook/signature.rs @@ -96,11 +96,6 @@ impl HookSignature { .type_params() .map(|ty_param| ty_param.ident.clone()) .collect() - - // let phantom_lifetimes = generics - // .lifetimes() - // .map(|life| quote! { &#life () }) - // .collect::>(); } pub fn phantom_lifetimes(&self) -> Vec { diff --git a/packages/yew/src/functional/hooks/use_ref.rs b/packages/yew/src/functional/hooks/use_ref.rs index f041bc33403..e605711dc93 100644 --- a/packages/yew/src/functional/hooks/use_ref.rs +++ b/packages/yew/src/functional/hooks/use_ref.rs @@ -1,4 +1,4 @@ -use crate::{functional::use_hook, NodeRef}; +use crate::{functional::use_hook, use_hook_next, NodeRef}; use std::{cell::RefCell, rc::Rc}; /// This hook is used for obtaining a mutable reference to a stateful value. @@ -188,5 +188,5 @@ pub fn use_node_ref() -> NodeRef { /// ``` #[crate::functional::hook] pub fn use_node_ref_next() -> NodeRef { - use_hook(NodeRef::default, |state, _| state.clone(), |_| {}) + use_hook_next(NodeRef::default, |state, _| state.clone(), |_| {}) } From aac78b1b438066a01a2319d4f04e8bfa2c5b0407 Mon Sep 17 00:00:00 2001 From: Kaede Hoshikawa Date: Sat, 22 Jan 2022 16:20:20 +0900 Subject: [PATCH 07/42] Port some built-in hooks. --- packages/yew-macro/src/hook/body.rs | 2 +- packages/yew-macro/src/hook/lifetime.rs | 16 +++ packages/yew-macro/src/hook/mod.rs | 31 ++++- packages/yew-macro/src/hook/signature.rs | 42 +++++- packages/yew/src/functional/hooks/mod.rs | 16 ++- .../yew/src/functional/hooks/use_context.rs | 65 ++++++++- .../yew/src/functional/hooks/use_effect.rs | 123 +++++++++++++++++- packages/yew/src/functional/mod.rs | 4 + 8 files changed, 284 insertions(+), 15 deletions(-) diff --git a/packages/yew-macro/src/hook/body.rs b/packages/yew-macro/src/hook/body.rs index 29cf4d520f7..f6b7fffdf64 100644 --- a/packages/yew-macro/src/hook/body.rs +++ b/packages/yew-macro/src/hook/body.rs @@ -37,7 +37,7 @@ impl VisitMut for BodyRewriter { if let Some(m) = m.path.segments.last().as_ref().map(|m| &m.ident) { if m.to_string().starts_with("use_") { if self.is_branched() { - emit_error!(i, "hooks cannot be called at this position."); + emit_error!(m, "hooks cannot be called at this position."); } else { *i = parse_quote_spanned! { i.span() => ::yew::functional::Hook::run(#i, #states_ident) }; } diff --git a/packages/yew-macro/src/hook/lifetime.rs b/packages/yew-macro/src/hook/lifetime.rs index f91410c5b04..e673b743f3d 100644 --- a/packages/yew-macro/src/hook/lifetime.rs +++ b/packages/yew-macro/src/hook/lifetime.rs @@ -27,6 +27,7 @@ pub struct CollectLifetimes { pub explicit: Vec, pub name: &'static str, pub default_span: Span, + // pub fn_ctr: u64, } impl CollectLifetimes { @@ -36,6 +37,7 @@ impl CollectLifetimes { explicit: Vec::new(), name, default_span, + // fn_ctr: 0, } } @@ -61,6 +63,20 @@ impl CollectLifetimes { self.elided.push(life.clone()); life } + + // fn is_in_fn(&self) -> bool { + // self.fn_ctr > 0 + // } + + // fn with_in_fn(&mut self, f: F) -> O + // where + // F: FnOnce(&mut CollectLifetimes) -> O, + // { + // self.fn_ctr += 1; + // let result = { f(self) }; + // self.fn_ctr -= 1; + // result + // } } impl VisitMut for CollectLifetimes { diff --git a/packages/yew-macro/src/hook/mod.rs b/packages/yew-macro/src/hook/mod.rs index cb4f731125e..bf4692246c9 100644 --- a/packages/yew-macro/src/hook/mod.rs +++ b/packages/yew-macro/src/hook/mod.rs @@ -89,6 +89,8 @@ When used in function components and hooks, this hook is equivalent to: let hook_struct_name = Ident::new("HookProvider", Span::mixed_site()); let (impl_generics, ty_generics, where_clause) = generics.split_for_impl(); + let call_generics = ty_generics.as_turbofish(); + let states = Ident::new("states", Span::mixed_site()); let phantom_types = hook_sig.phantom_types(); @@ -98,19 +100,28 @@ When used in function components and hooks, this hook is equivalent to: visit_mut::visit_block_mut(&mut body_rewriter, &mut *block); let hook_lifetime_plus = hook_lifetime.map(|m| quote! { #m + }); + let inner_ident = Ident::new("inner", Span::mixed_site()); + + // let inner_fn_ident = Ident::new("inner_fn", Span::mixed_site()); + // let input_args = hook_sig.input_args(); let output = quote! { #(#attrs)* #[doc = #doc_text] - #vis #fn_token #ident #generics (#inputs) #hook_return_type { + #vis #fn_token #ident #generics (#inputs) #hook_return_type #where_clause { + // fn #inner_fn_ident #generics (#states: &mut ::yew::functional::HookStates, #inputs) -> #output_type #block + // always capture inputs with closure for now, we need boxing implementation for `impl Trait` // arguments anyways. - let inner = ::std::boxed::Box::new(move |#states: &mut ::yew::functional::HookStates| #block) + // let inner = ::std::boxed::Box::new(move |#states: &mut ::yew::functional::HookStates| #inner_fn_ident #call_generics (#states, #(#input_args)*) ) + // as ::std::boxed::Box<#hook_lifetime_plus FnOnce(&mut ::yew::functional::HookStates) -> #output_type>; + + let #inner_ident = ::std::boxed::Box::new(move |#states: &mut ::yew::functional::HookStates| #block ) as ::std::boxed::Box<#hook_lifetime_plus FnOnce(&mut ::yew::functional::HookStates) -> #output_type>; - struct #hook_struct_name #generics { + struct #hook_struct_name #generics #where_clause { _marker: ::std::marker::PhantomData<( #(#phantom_types,)* #(#phantom_lifetimes,)* )>, - inner: ::std::boxed::Box<#hook_lifetime_plus FnOnce(&mut ::yew::functional::HookStates) -> #output_type> + #inner_ident: ::std::boxed::Box<#hook_lifetime_plus FnOnce(&mut ::yew::functional::HookStates) -> #output_type> } impl #impl_generics ::yew::functional::Hook for #hook_struct_name #ty_generics #where_clause { @@ -121,10 +132,16 @@ When used in function components and hooks, this hook is equivalent to: } } - #hook_struct_name { - _marker: ::std::marker::PhantomData, - inner, + impl #impl_generics #hook_struct_name #ty_generics #where_clause { + fn new(#inner_ident: ::std::boxed::Box<#hook_lifetime_plus FnOnce(&mut ::yew::functional::HookStates) -> #output_type>) -> Self { + #hook_struct_name { + _marker: ::std::marker::PhantomData, + #inner_ident, + } + } } + + #hook_struct_name #call_generics ::new(#inner_ident) } }; diff --git a/packages/yew-macro/src/hook/signature.rs b/packages/yew-macro/src/hook/signature.rs index f1646ec8ed0..a3c8606cfcf 100644 --- a/packages/yew-macro/src/hook/signature.rs +++ b/packages/yew-macro/src/hook/signature.rs @@ -1,9 +1,10 @@ use proc_macro2::Span; use quote::quote; use syn::spanned::Spanned; +use syn::visit_mut::VisitMut; use syn::{ - parse_quote, parse_quote_spanned, token, visit_mut, Ident, Lifetime, ReturnType, Signature, - Type, TypeReference, WhereClause, + parse_quote, parse_quote_spanned, token, FnArg, Ident, Lifetime, ReturnType, Signature, Type, + TypeReference, WhereClause, }; use super::lifetime; @@ -23,7 +24,7 @@ impl HookSignature { match rt_type { ReturnType::Default => ( - parse_quote! { -> impl #bound ::yew::functional::Hook }, + parse_quote! { -> impl #bound ::yew::functional::Hook }, parse_quote! { () }, ), ReturnType::Type(arrow, ref return_type) => ( @@ -40,7 +41,12 @@ impl HookSignature { let mut sig = sig.clone(); let mut lifetimes = lifetime::CollectLifetimes::new("'arg", sig.ident.span()); - visit_mut::visit_signature_mut(&mut lifetimes, &mut sig); + for arg in sig.inputs.iter_mut() { + match arg { + FnArg::Receiver(arg) => lifetimes.visit_receiver_mut(arg), + FnArg::Typed(arg) => lifetimes.visit_type_mut(&mut arg.ty), + } + } let Signature { ref mut generics, @@ -73,6 +79,18 @@ impl HookSignature { .push(parse_quote!(#elided: #hook_lifetime)); } + for explicit in lifetimes.explicit.iter() { + where_clause + .predicates + .push(parse_quote!(#explicit: #hook_lifetime)); + } + + for type_param in generics.type_params() { + where_clause + .predicates + .push(parse_quote!(#type_param: #hook_lifetime)); + } + generics.where_clause = Some(where_clause); Some(hook_lifetime) @@ -105,4 +123,20 @@ impl HookSignature { .map(|life| parse_quote! { &#life () }) .collect() } + + // pub fn input_args(&self) -> Vec { + // self.sig + // .inputs + // .iter() + // .filter_map(|m| { + // if let FnArg::Typed(m) = m { + // if let Pat::Ident(ref m) = *m.pat { + // return Some(m.ident.clone()); + // } + // } + + // None + // }) + // .collect() + // } } diff --git a/packages/yew/src/functional/hooks/mod.rs b/packages/yew/src/functional/hooks/mod.rs index e22f85b889d..cff2b806a6a 100644 --- a/packages/yew/src/functional/hooks/mod.rs +++ b/packages/yew/src/functional/hooks/mod.rs @@ -10,7 +10,7 @@ pub use use_reducer::*; pub use use_ref::*; pub use use_state::*; -use crate::functional::{HookStates, HookUpdater, CURRENT_HOOK}; +use crate::functional::{AnyScope, HookStates, HookUpdater, CURRENT_HOOK}; /// A trait that is implemented on hooks. /// @@ -115,3 +115,17 @@ where destructor: Box::new(destructor), } } + +pub(crate) fn use_component_scope() -> impl Hook { + struct HookProvider {} + + impl Hook for HookProvider { + type Output = AnyScope; + + fn run(self, states: &mut HookStates) -> Self::Output { + states.scope().clone() + } + } + + HookProvider {} +} diff --git a/packages/yew/src/functional/hooks/use_context.rs b/packages/yew/src/functional/hooks/use_context.rs index 581f4274264..223ab583831 100644 --- a/packages/yew/src/functional/hooks/use_context.rs +++ b/packages/yew/src/functional/hooks/use_context.rs @@ -1,5 +1,5 @@ use crate::context::ContextHandle; -use crate::functional::{get_current_scope, use_hook}; +use crate::functional::{get_current_scope, hook, use_component_scope, use_hook, use_hook_next}; /// Hook for consuming context values in function components. /// The context of the type passed as `T` is returned. If there is no such context in scope, `None` is returned. @@ -63,3 +63,66 @@ pub fn use_context() -> Option { }, ) } + +/// Hook for consuming context values in function components. +/// The context of the type passed as `T` is returned. If there is no such context in scope, `None` is returned. +/// A component which calls `use_context` will re-render when the data of the context changes. +/// +/// More information about contexts and how to define and consume them can be found on [Yew Docs](https://yew.rs). +/// +/// # Example +/// ```rust +/// # use yew::prelude::*; +/// # use std::rc::Rc; +/// +/// # #[derive(Clone, Debug, PartialEq)] +/// # struct ThemeContext { +/// # foreground: String, +/// # background: String, +/// # } +/// #[function_component(ThemedButton)] +/// pub fn themed_button() -> Html { +/// let theme = use_context::>().expect("no ctx found"); +/// +/// html! { +/// +/// } +/// } +/// ``` +#[hook] +pub fn use_context_next() -> Option { + struct UseContextState { + initialized: bool, + context: Option<(T2, ContextHandle)>, + } + + let scope = use_component_scope(); + + use_hook_next( + move || UseContextState { + initialized: false, + context: None, + }, + |state: &mut UseContextState, updater| { + if !state.initialized { + state.initialized = true; + let callback = move |ctx: T| { + updater.callback(|state: &mut UseContextState| { + if let Some(context) = &mut state.context { + context.0 = ctx; + } + true + }); + }; + state.context = scope.context::(callback.into()); + } + + Some(state.context.as_ref()?.0.clone()) + }, + |state| { + state.context = None; + }, + ) +} diff --git a/packages/yew/src/functional/hooks/use_effect.rs b/packages/yew/src/functional/hooks/use_effect.rs index da68def20f3..08a5967b783 100644 --- a/packages/yew/src/functional/hooks/use_effect.rs +++ b/packages/yew/src/functional/hooks/use_effect.rs @@ -1,4 +1,4 @@ -use crate::functional::use_hook; +use crate::functional::{hook, use_hook, use_hook_next}; use std::rc::Rc; struct UseEffect { @@ -72,6 +72,73 @@ where ) } +/// This hook is used for hooking into the component's lifecycle. +/// +/// # Example +/// ```rust +/// # use yew::prelude::*; +/// # use std::rc::Rc; +/// # +/// #[function_component(UseEffect)] +/// fn effect() -> Html { +/// let counter = use_state(|| 0); +/// +/// let counter_one = counter.clone(); +/// use_effect(move || { +/// // Make a call to DOM API after component is rendered +/// gloo_utils::document().set_title(&format!("You clicked {} times", *counter_one)); +/// +/// // Perform the cleanup +/// || gloo_utils::document().set_title(&format!("You clicked 0 times")) +/// }); +/// +/// let onclick = { +/// let counter = counter.clone(); +/// Callback::from(move |_| counter.set(*counter + 1)) +/// }; +/// +/// html! { +/// +/// } +/// } +/// ``` +#[hook] +pub fn use_effect_next(callback: impl FnOnce() -> Destructor + 'static) +where + Destructor: FnOnce() + 'static, +{ + use_hook_next( + move || { + let effect: UseEffect = UseEffect { + runner: None, + destructor: None, + }; + effect + }, + |state, updater| { + state.runner = Some(Box::new(callback) as Box Destructor>); + + // Run on every render + updater.post_render(move |state: &mut UseEffect| { + if let Some(callback) = state.runner.take() { + if let Some(de) = state.destructor.take() { + de(); + } + + let new_destructor = callback(); + state.destructor.replace(Box::new(new_destructor)); + } + false + }); + }, + |hook| { + if let Some(destructor) = hook.destructor.take() { + destructor() + } + }, + ) +} + type UseEffectDepsRunnerFn = Box Destructor>; struct UseEffectDeps { @@ -133,3 +200,57 @@ where }, ); } + +/// This hook is similar to [`use_effect`] but it accepts dependencies. +/// +/// Whenever the dependencies are changed, the effect callback is called again. +/// To detect changes, dependencies must implement `PartialEq`. +/// Note that the destructor also runs when dependencies change. +#[hook] +pub fn use_effect_with_deps_next( + callback: Callback, + deps: Dependents, +) where + Callback: FnOnce(&Dependents) -> Destructor + 'static, + Destructor: FnOnce() + 'static, + Dependents: PartialEq + 'static, +{ + let deps = Rc::new(deps); + + use_hook_next( + move || { + let destructor: Option> = None; + UseEffectDeps { + runner_with_deps: None, + destructor, + deps: None, + } + }, + move |state, updater| { + state.runner_with_deps = Some((deps, Box::new(callback))); + + updater.post_render(move |state: &mut UseEffectDeps| { + if let Some((deps, callback)) = state.runner_with_deps.take() { + if Some(&deps) == state.deps.as_ref() { + return false; + } + + if let Some(de) = state.destructor.take() { + de(); + } + + let new_destructor = callback(&deps); + + state.deps = Some(deps); + state.destructor = Some(Box::new(new_destructor)); + } + false + }); + }, + |hook| { + if let Some(destructor) = hook.destructor.take() { + destructor() + } + }, + ); +} diff --git a/packages/yew/src/functional/mod.rs b/packages/yew/src/functional/mod.rs index 456218dac66..26bb5ec680c 100644 --- a/packages/yew/src/functional/mod.rs +++ b/packages/yew/src/functional/mod.rs @@ -108,6 +108,10 @@ impl HookStates { process_message: self.process_message.clone(), } } + + fn scope(&self) -> &AnyScope { + &self.scope + } } impl fmt::Debug for HookStates { From 1e5c373f44b515f84f9d02fa23c2c6374bd6fdc3 Mon Sep 17 00:00:00 2001 From: Kaede Hoshikawa Date: Sat, 22 Jan 2022 16:59:22 +0900 Subject: [PATCH 08/42] Finish porting all built-in hooks. --- packages/yew-macro/src/hook/lifetime.rs | 19 --- packages/yew-macro/src/hook/mod.rs | 6 +- packages/yew-macro/src/hook/signature.rs | 7 +- .../yew/src/functional/hooks/use_reducer.rs | 141 +++++++++++++++++- packages/yew/src/functional/hooks/use_ref.rs | 74 ++++++++- .../yew/src/functional/hooks/use_state.rs | 62 +++++++- 6 files changed, 280 insertions(+), 29 deletions(-) diff --git a/packages/yew-macro/src/hook/lifetime.rs b/packages/yew-macro/src/hook/lifetime.rs index e673b743f3d..fa2eaaa39db 100644 --- a/packages/yew-macro/src/hook/lifetime.rs +++ b/packages/yew-macro/src/hook/lifetime.rs @@ -2,25 +2,6 @@ use proc_macro2::Span; use syn::visit_mut::{self, VisitMut}; use syn::{GenericArgument, Lifetime, Receiver, TypeReference}; -/// Finds an unused lifetime. -pub fn find_available_lifetime(lifetimes: &CollectLifetimes) -> Lifetime { - for i in 0.. { - let hook_lifetime = if i == 0 { - Lifetime::new("'hook", lifetimes.default_span) - } else { - Lifetime::new(&format!("'hook{}", i), lifetimes.default_span) - }; - - if !lifetimes.elided.contains(&hook_lifetime) - && !lifetimes.explicit.contains(&hook_lifetime) - { - return hook_lifetime; - } - } - - unreachable!() -} - // borrowed from the async-trait crate. pub struct CollectLifetimes { pub elided: Vec, diff --git a/packages/yew-macro/src/hook/mod.rs b/packages/yew-macro/src/hook/mod.rs index bf4692246c9..d86322bff54 100644 --- a/packages/yew-macro/src/hook/mod.rs +++ b/packages/yew-macro/src/hook/mod.rs @@ -117,11 +117,11 @@ When used in function components and hooks, this hook is equivalent to: // as ::std::boxed::Box<#hook_lifetime_plus FnOnce(&mut ::yew::functional::HookStates) -> #output_type>; let #inner_ident = ::std::boxed::Box::new(move |#states: &mut ::yew::functional::HookStates| #block ) - as ::std::boxed::Box<#hook_lifetime_plus FnOnce(&mut ::yew::functional::HookStates) -> #output_type>; + as ::std::boxed::Box #output_type>; struct #hook_struct_name #generics #where_clause { _marker: ::std::marker::PhantomData<( #(#phantom_types,)* #(#phantom_lifetimes,)* )>, - #inner_ident: ::std::boxed::Box<#hook_lifetime_plus FnOnce(&mut ::yew::functional::HookStates) -> #output_type> + #inner_ident: ::std::boxed::Box #output_type> } impl #impl_generics ::yew::functional::Hook for #hook_struct_name #ty_generics #where_clause { @@ -133,7 +133,7 @@ When used in function components and hooks, this hook is equivalent to: } impl #impl_generics #hook_struct_name #ty_generics #where_clause { - fn new(#inner_ident: ::std::boxed::Box<#hook_lifetime_plus FnOnce(&mut ::yew::functional::HookStates) -> #output_type>) -> Self { + fn new(#inner_ident: ::std::boxed::Box #output_type>) -> Self { #hook_struct_name { _marker: ::std::marker::PhantomData, #inner_ident, diff --git a/packages/yew-macro/src/hook/signature.rs b/packages/yew-macro/src/hook/signature.rs index a3c8606cfcf..30a07777d1a 100644 --- a/packages/yew-macro/src/hook/signature.rs +++ b/packages/yew-macro/src/hook/signature.rs @@ -54,8 +54,8 @@ impl HookSignature { .. } = sig; - let hook_lifetime = if !lifetimes.elided.is_empty() { - let hook_lifetime = lifetime::find_available_lifetime(&lifetimes); + let hook_lifetime = if !generics.params.is_empty() { + let hook_lifetime = Lifetime::new("'hook", Span::mixed_site()); generics.params = { let elided_lifetimes = &lifetimes.elided; let params = &generics.params; @@ -86,9 +86,10 @@ impl HookSignature { } for type_param in generics.type_params() { + let type_param_ident = &type_param.ident; where_clause .predicates - .push(parse_quote!(#type_param: #hook_lifetime)); + .push(parse_quote!(#type_param_ident: #hook_lifetime)); } generics.where_clause = Some(where_clause); diff --git a/packages/yew/src/functional/hooks/use_reducer.rs b/packages/yew/src/functional/hooks/use_reducer.rs index 99ea78c3e67..847c573e24e 100644 --- a/packages/yew/src/functional/hooks/use_reducer.rs +++ b/packages/yew/src/functional/hooks/use_reducer.rs @@ -3,7 +3,7 @@ use std::fmt; use std::ops::Deref; use std::rc::Rc; -use crate::functional::use_hook; +use crate::functional::{hook, use_hook, use_hook_next}; type DispatchFn = Rc::Action)>; @@ -191,6 +191,55 @@ where ) } +/// The base function of [`use_reducer`] and [`use_reducer_eq`] +#[hook] +fn use_reducer_base_next(initial_fn: F, should_render_fn: R) -> UseReducerHandle +where + T: Reducible + 'static, + F: FnOnce() -> T, + R: (Fn(&T, &T) -> bool) + 'static, +{ + use_hook_next( + move || UseReducer { + current_state: Rc::new(initial_fn()), + dispatch: RefCell::default(), + }, + |s, updater| { + let mut dispatch_ref = s.dispatch.borrow_mut(); + + // Create dispatch once. + let dispatch = match *dispatch_ref { + Some(ref m) => (*m).to_owned(), + None => { + let should_render_fn = Rc::new(should_render_fn); + + let dispatch: Rc = Rc::new(move |action: T::Action| { + let should_render_fn = should_render_fn.clone(); + + updater.callback(move |state: &mut UseReducer| { + let next_state = state.current_state.clone().reduce(action); + let should_render = should_render_fn(&next_state, &state.current_state); + state.current_state = next_state; + + should_render + }); + }); + + *dispatch_ref = Some(dispatch.clone()); + + dispatch + } + }; + + UseReducerHandle { + value: Rc::clone(&s.current_state), + dispatch, + } + }, + |_| {}, + ) +} + /// This hook is an alternative to [`use_state`](super::use_state()). /// It is used to handle component's state and is used when complex actions needs to be performed on said state. /// @@ -278,3 +327,93 @@ where { use_reducer_base(initial_fn, T::ne) } + +/// This hook is an alternative to [`use_state`](super::use_state()). +/// It is used to handle component's state and is used when complex actions needs to be performed on said state. +/// +/// The state is expected to implement the [`Reducible`] trait which provides an `Action` type and a reducer +/// function. +/// +/// # Example +/// ```rust +/// # use yew::prelude::*; +/// # use std::rc::Rc; +/// # +/// +/// /// reducer's Action +/// enum CounterAction { +/// Double, +/// Square, +/// } +/// +/// /// reducer's State +/// struct CounterState { +/// counter: i32, +/// } +/// +/// impl Default for CounterState { +/// fn default() -> Self { +/// Self { counter: 1 } +/// } +/// } +/// +/// impl Reducible for CounterState { +/// /// Reducer Action Type +/// type Action = CounterAction; +/// +/// /// Reducer Function +/// fn reduce(self: Rc, action: Self::Action) -> Rc { +/// let next_ctr = match action { +/// CounterAction::Double => self.counter * 2, +/// CounterAction::Square => self.counter.pow(2) +/// }; +/// +/// Self { counter: next_ctr }.into() +/// } +/// } +/// +/// #[function_component(UseReducer)] +/// fn reducer() -> Html { +/// // The use_reducer hook takes an initialization function which will be called only once. +/// let counter = use_reducer(CounterState::default); +/// +/// let double_onclick = { +/// let counter = counter.clone(); +/// Callback::from(move |_| counter.dispatch(CounterAction::Double)) +/// }; +/// let square_onclick = { +/// let counter = counter.clone(); +/// Callback::from(move |_| counter.dispatch(CounterAction::Square)) +/// }; +/// +/// html! { +/// <> +///
{ counter.counter }
+/// +/// +/// +/// +/// } +/// } +/// ``` +#[hook] +pub fn use_reducer_next(initial_fn: F) -> UseReducerHandle +where + T: Reducible + 'static, + F: FnOnce() -> T, +{ + use_reducer_base_next(initial_fn, |_, _| true) +} + +/// [`use_reducer`] but only re-renders when `prev_state != next_state`. +/// +/// This requires the state to implement [`PartialEq`] in addition to the [`Reducible`] trait +/// required by [`use_reducer`]. +#[hook] +pub fn use_reducer_eq_next(initial_fn: F) -> UseReducerHandle +where + T: Reducible + PartialEq + 'static, + F: FnOnce() -> T, +{ + use_reducer_base_next(initial_fn, T::ne) +} diff --git a/packages/yew/src/functional/hooks/use_ref.rs b/packages/yew/src/functional/hooks/use_ref.rs index e605711dc93..aa375317b50 100644 --- a/packages/yew/src/functional/hooks/use_ref.rs +++ b/packages/yew/src/functional/hooks/use_ref.rs @@ -1,4 +1,5 @@ -use crate::{functional::use_hook, use_hook_next, NodeRef}; +use crate::functional::{hook, use_hook, use_hook_next}; +use crate::NodeRef; use std::{cell::RefCell, rc::Rc}; /// This hook is used for obtaining a mutable reference to a stateful value. @@ -55,6 +56,61 @@ pub fn use_mut_ref(initial_value: impl FnOnce() -> T) -> Rc Html { +/// let message = use_state(|| "".to_string()); +/// let message_count = use_mut_ref(|| 0); +/// +/// let onclick = Callback::from(move |e| { +/// let window = gloo_utils::window(); +/// +/// if *message_count.borrow_mut() > 3 { +/// window.alert_with_message("Message limit reached"); +/// } else { +/// *message_count.borrow_mut() += 1; +/// window.alert_with_message("Message sent"); +/// } +/// }); +/// +/// let onchange = { +/// let message = message.clone(); +/// Callback::from(move |e: Event| { +/// let input: HtmlInputElement = e.target_unchecked_into(); +/// message.set(input.value()) +/// }) +/// }; +/// +/// html! { +///
+/// +/// +///
+/// } +/// } +/// ``` +#[hook] +pub fn use_mut_ref_next(initial_value: impl 'hook + FnOnce() -> T) -> Rc> { + use_hook_next( + || Rc::new(RefCell::new(initial_value())), + |state, _| state.clone(), + |_| {}, + ) +} + /// This hook is used for obtaining a immutable reference to a stateful value. /// Its state persists across renders. /// @@ -68,6 +124,20 @@ pub fn use_ref(initial_value: impl FnOnce() -> T) -> Rc { ) } +/// This hook is used for obtaining a immutable reference to a stateful value. +/// Its state persists across renders. +/// +/// If you need a mutable reference, consider using [`use_mut_ref`](super::use_mut_ref). +/// If you need the component to be re-rendered on state change, consider using [`use_state`](super::use_state()). +#[hook] +pub fn use_ref_next(initial_value: impl 'hook + FnOnce() -> T) -> Rc { + use_hook_next( + || Rc::new(initial_value()), + |state, _| Rc::clone(state), + |_| {}, + ) +} + /// This hook is used for obtaining a [`NodeRef`]. /// It persists across renders. /// @@ -186,7 +256,7 @@ pub fn use_node_ref() -> NodeRef { /// } /// /// ``` -#[crate::functional::hook] +#[hook] pub fn use_node_ref_next() -> NodeRef { use_hook_next(NodeRef::default, |state, _| state.clone(), |_| {}) } diff --git a/packages/yew/src/functional/hooks/use_state.rs b/packages/yew/src/functional/hooks/use_state.rs index ae3f7f0fd5e..ea79a659aa1 100644 --- a/packages/yew/src/functional/hooks/use_state.rs +++ b/packages/yew/src/functional/hooks/use_state.rs @@ -2,7 +2,11 @@ use std::fmt; use std::ops::Deref; use std::rc::Rc; -use super::{use_reducer, use_reducer_eq, Reducible, UseReducerDispatcher, UseReducerHandle}; +use super::{ + use_reducer, use_reducer_eq, use_reducer_eq_next, use_reducer_next, Reducible, + UseReducerDispatcher, UseReducerHandle, +}; +use crate::functional::hook; struct UseStateReducer { value: Rc, @@ -65,6 +69,46 @@ where UseStateHandle { inner: handle } } +/// This hook is used to manage state in a function component. +/// +/// # Example +/// ```rust +/// # use yew::prelude::*; +/// # use std::rc::Rc; +/// # +/// #[function_component(UseState)] +/// fn state() -> Html { +/// let counter = use_state(|| 0); +/// let onclick = { +/// let counter = counter.clone(); +/// Callback::from(move |_| counter.set(*counter + 1)) +/// }; +/// +/// +/// html! { +///
+/// +///

+/// { "Current value: " } +/// { *counter } +///

+///
+/// } +/// } +/// ``` +#[hook] +pub fn use_state_next(init_fn: F) -> UseStateHandle +where + T: 'static, + F: FnOnce() -> T, +{ + let handle = use_reducer_next(move || UseStateReducer { + value: Rc::new(init_fn()), + }); + + UseStateHandle { inner: handle } +} + /// [`use_state`] but only re-renders when `prev_state != next_state`. /// /// This hook requires the state to implement [`PartialEq`]. @@ -80,6 +124,22 @@ where UseStateHandle { inner: handle } } +/// [`use_state`] but only re-renders when `prev_state != next_state`. +/// +/// This hook requires the state to implement [`PartialEq`]. +#[hook] +pub fn use_state_eq_next(init_fn: F) -> UseStateHandle +where + T: PartialEq + 'static, + F: FnOnce() -> T, +{ + let handle = use_reducer_eq_next(move || UseStateReducer { + value: Rc::new(init_fn()), + }); + + UseStateHandle { inner: handle } +} + /// State handle for the [`use_state`] hook. pub struct UseStateHandle { inner: UseReducerHandle>, From 5f086e87fb48151ebdb367107e97c26e52baa937 Mon Sep 17 00:00:00 2001 From: Kaede Hoshikawa Date: Sat, 22 Jan 2022 17:34:36 +0900 Subject: [PATCH 09/42] Port tests. --- packages/yew-macro/src/hook/mod.rs | 18 +- packages/yew/src/functional/hooks/mod.rs | 11 +- packages/yew/tests/use_context.rs | 354 ++++++++++------------- packages/yew/tests/use_effect.rs | 235 +++++++-------- packages/yew/tests/use_reducer.rs | 110 ++++--- packages/yew/tests/use_ref.rs | 37 +-- packages/yew/tests/use_state.rs | 130 ++++----- 7 files changed, 402 insertions(+), 493 deletions(-) diff --git a/packages/yew-macro/src/hook/mod.rs b/packages/yew-macro/src/hook/mod.rs index d86322bff54..da190ee43ba 100644 --- a/packages/yew-macro/src/hook/mod.rs +++ b/packages/yew-macro/src/hook/mod.rs @@ -4,7 +4,7 @@ use quote::quote; use quote::ToTokens; use syn::parse::{Parse, ParseStream}; use syn::visit_mut; -use syn::{Ident, ItemFn, LitStr, Signature}; +use syn::{Ident, ItemFn, LitStr, ReturnType, Signature}; mod body; mod lifetime; @@ -104,10 +104,18 @@ When used in function components and hooks, this hook is equivalent to: // let inner_fn_ident = Ident::new("inner_fn", Span::mixed_site()); // let input_args = hook_sig.input_args(); + let boxed_fn_type = { + let rt = match &sig.output { + ReturnType::Default => None, + ReturnType::Type(_, _) => Some(quote! { -> #output_type }), + }; + + quote! { ::std::boxed::Box } + }; let output = quote! { - #(#attrs)* #[doc = #doc_text] + #(#attrs)* #vis #fn_token #ident #generics (#inputs) #hook_return_type #where_clause { // fn #inner_fn_ident #generics (#states: &mut ::yew::functional::HookStates, #inputs) -> #output_type #block @@ -117,11 +125,11 @@ When used in function components and hooks, this hook is equivalent to: // as ::std::boxed::Box<#hook_lifetime_plus FnOnce(&mut ::yew::functional::HookStates) -> #output_type>; let #inner_ident = ::std::boxed::Box::new(move |#states: &mut ::yew::functional::HookStates| #block ) - as ::std::boxed::Box #output_type>; + as #boxed_fn_type; struct #hook_struct_name #generics #where_clause { _marker: ::std::marker::PhantomData<( #(#phantom_types,)* #(#phantom_lifetimes,)* )>, - #inner_ident: ::std::boxed::Box #output_type> + #inner_ident: #boxed_fn_type, } impl #impl_generics ::yew::functional::Hook for #hook_struct_name #ty_generics #where_clause { @@ -133,7 +141,7 @@ When used in function components and hooks, this hook is equivalent to: } impl #impl_generics #hook_struct_name #ty_generics #where_clause { - fn new(#inner_ident: ::std::boxed::Box #output_type>) -> Self { + fn new(#inner_ident: #boxed_fn_type) -> Self { #hook_struct_name { _marker: ::std::marker::PhantomData, #inner_ident, diff --git a/packages/yew/src/functional/hooks/mod.rs b/packages/yew/src/functional/hooks/mod.rs index cff2b806a6a..6bb81a1f6ec 100644 --- a/packages/yew/src/functional/hooks/mod.rs +++ b/packages/yew/src/functional/hooks/mod.rs @@ -60,10 +60,15 @@ pub fn use_hook( initializer: INIT, runner: RUN, diff --git a/packages/yew/tests/use_context.rs b/packages/yew/tests/use_context.rs index 4fffc969e94..1ed7d73ae82 100644 --- a/packages/yew/tests/use_context.rs +++ b/packages/yew/tests/use_context.rs @@ -3,10 +3,7 @@ mod common; use common::obtain_result_by_id; use std::rc::Rc; use wasm_bindgen_test::*; -use yew::functional::{ - use_context, use_effect, use_mut_ref, use_state, FunctionComponent, FunctionProvider, -}; -use yew::{html, Children, ContextProvider, HtmlResult, Properties}; +use yew::prelude::*; wasm_bindgen_test::wasm_bindgen_test_configure!(run_in_browser); @@ -14,62 +11,50 @@ wasm_bindgen_test::wasm_bindgen_test_configure!(run_in_browser); fn use_context_scoping_works() { #[derive(Clone, Debug, PartialEq)] struct ExampleContext(String); - struct UseContextFunctionOuter {} - struct UseContextFunctionInner {} - struct ExpectNoContextFunction {} - type UseContextComponent = FunctionComponent; - type UseContextComponentInner = FunctionComponent; - type ExpectNoContextComponent = FunctionComponent; - impl FunctionProvider for ExpectNoContextFunction { - type TProps = (); - - fn run(_props: &Self::TProps) -> HtmlResult { - if use_context::().is_some() { - console_log!( - "Context should be None here, but was {:?}!", - use_context::().unwrap() - ); - }; - Ok(html! { -
- }) - } + + #[function_component] + fn ExpectNoContextComponent() -> Html { + if use_context::().is_some() { + console_log!( + "Context should be None here, but was {:?}!", + use_context::().unwrap() + ); + }; + Ok(html! { +
+ }) } - impl FunctionProvider for UseContextFunctionOuter { - type TProps = (); - - fn run(_props: &Self::TProps) -> HtmlResult { - type ExampleContextProvider = ContextProvider; - Ok(html! { -
- -
{"ignored"}
-
- - - -
{"ignored"}
-
- + + #[function_component] + fn UseContextComponent() -> Html { + type ExampleContextProvider = ContextProvider; + Ok(html! { +
+ +
{"ignored"}
+
+ + + +
{"ignored"}
+
- -
{"ignored"}
-
- -
- }) - } +
+ +
{"ignored"}
+
+ +
+ }) } - impl FunctionProvider for UseContextFunctionInner { - type TProps = (); - - fn run(_props: &Self::TProps) -> HtmlResult { - let context = use_context::(); - Ok(html! { -
{ &context.unwrap().0 }
- }) - } + + #[function_component] + fn UseContextComponentInner() -> Html { + let context = use_context::(); + Ok(html! { +
{ &context.unwrap().0 }
+ }) } yew::start_app_in_element::( @@ -86,83 +71,58 @@ fn use_context_works_with_multiple_types() { #[derive(Clone, Debug, PartialEq)] struct ContextB(u32); - struct Test1Function; - impl FunctionProvider for Test1Function { - type TProps = (); + #[function_component] + fn Test1() -> Html { + assert_eq!(use_context::(), Some(ContextA(0))); + assert_eq!(use_context::(), Some(ContextB(1))); - fn run(_props: &Self::TProps) -> HtmlResult { - assert_eq!(use_context::(), Some(ContextA(2))); - assert_eq!(use_context::(), Some(ContextB(1))); - - Ok(html! {}) - } + Ok(html! {}) } - type Test1 = FunctionComponent; - struct Test2Function; - impl FunctionProvider for Test2Function { - type TProps = (); + #[function_component] + fn Test2() -> Html { + assert_eq!(use_context::(), Some(ContextA(0))); + assert_eq!(use_context::(), Some(ContextB(1))); - fn run(_props: &Self::TProps) -> HtmlResult { - assert_eq!(use_context::(), Some(ContextA(0))); - assert_eq!(use_context::(), Some(ContextB(1))); - - Ok(html! {}) - } + Ok(html! {}) } - type Test2 = FunctionComponent; - - struct Test3Function; - impl FunctionProvider for Test3Function { - type TProps = (); - fn run(_props: &Self::TProps) -> HtmlResult { - assert_eq!(use_context::(), Some(ContextA(0))); - assert_eq!(use_context::(), None); + #[function_component] + fn Test3() -> Html { + assert_eq!(use_context::(), Some(ContextA(0))); + assert_eq!(use_context::(), None); - Ok(html! {}) - } + Ok(html! {}) } - type Test3 = FunctionComponent; - - struct Test4Function; - impl FunctionProvider for Test4Function { - type TProps = (); - fn run(_props: &Self::TProps) -> HtmlResult { - assert_eq!(use_context::(), None); - assert_eq!(use_context::(), None); + #[function_component] + fn Test4() -> Html { + assert_eq!(use_context::(), None); + assert_eq!(use_context::(), None); - Ok(html! {}) - } + Ok(html! {}) } - type Test4 = FunctionComponent; - - struct TestFunction; - impl FunctionProvider for TestFunction { - type TProps = (); - - fn run(_props: &Self::TProps) -> HtmlResult { - type ContextAProvider = ContextProvider; - type ContextBProvider = ContextProvider; - - Ok(html! { -
- - - - - - - - - - -
- }) - } + + #[function_component] + fn TestComponent() -> Html { + type ContextAProvider = ContextProvider; + type ContextBProvider = ContextProvider; + + Ok(html! { +
+ + + + + + + + + + +
+ }) } - type TestComponent = FunctionComponent; yew::start_app_in_element::( gloo_utils::document().get_element_by_id("output").unwrap(), @@ -180,24 +140,19 @@ fn use_context_update_works() { children: Children, } - struct RenderCounterFunction; - impl FunctionProvider for RenderCounterFunction { - type TProps = RenderCounterProps; - - fn run(props: &Self::TProps) -> HtmlResult { - let counter = use_mut_ref(|| 0); - *counter.borrow_mut() += 1; - Ok(html! { - <> -
- { format!("total: {}", counter.borrow()) } -
- { props.children.clone() } - - }) - } + #[function_component] + fn RenderCounter(props: &RenderCounterProps) -> Html { + let counter = use_mut_ref(|| 0); + *counter.borrow_mut() += 1; + Ok(html! { + <> +
+ { format!("total: {}", counter.borrow()) } +
+ { props.children.clone() } + + }) } - type RenderCounter = FunctionComponent; #[derive(Clone, Debug, PartialEq, Properties)] struct ContextOutletProps { @@ -205,75 +160,66 @@ fn use_context_update_works() { #[prop_or_default] magic: usize, } - struct ContextOutletFunction; - impl FunctionProvider for ContextOutletFunction { - type TProps = ContextOutletProps; - - fn run(props: &Self::TProps) -> HtmlResult { - let counter = use_mut_ref(|| 0); - *counter.borrow_mut() += 1; - - let ctx = use_context::>().expect("context not passed down"); - - Ok(html! { - <> -
{ format!("magic: {}\n", props.magic) }
-
- { format!("current: {}, total: {}", ctx.0, counter.borrow()) } -
- - }) - } + + #[function_component] + fn ContextOutlet(props: &ContextOutletProps) -> Html { + let counter = use_mut_ref(|| 0); + *counter.borrow_mut() += 1; + + let ctx = use_context::>().expect("context not passed down"); + + Ok(html! { + <> +
{ format!("magic: {}\n", props.magic) }
+
+ { format!("current: {}, total: {}", ctx.0, counter.borrow()) } +
+ + }) } - type ContextOutlet = FunctionComponent; - - struct TestFunction; - impl FunctionProvider for TestFunction { - type TProps = (); - - fn run(_props: &Self::TProps) -> HtmlResult { - type MyContextProvider = ContextProvider>; - - let ctx = use_state(|| MyContext("hello".into())); - let rendered = use_mut_ref(|| 0); - - // this is used to force an update specific to test-2 - let magic_rc = use_state(|| 0); - let magic: usize = *magic_rc; - { - let ctx = ctx.clone(); - use_effect(move || { - let count = *rendered.borrow(); - match count { - 0 => { - ctx.set(MyContext("world".into())); - *rendered.borrow_mut() += 1; - } - 1 => { - // force test-2 to re-render. - magic_rc.set(1); - *rendered.borrow_mut() += 1; - } - 2 => { - ctx.set(MyContext("hello world!".into())); - *rendered.borrow_mut() += 1; - } - _ => (), - }; - || {} - }); - } - Ok(html! { - - - - - - - }) + + #[function_component] + fn TestComponent() -> Html { + type MyContextProvider = ContextProvider>; + + let ctx = use_state(|| MyContext("hello".into())); + let rendered = use_mut_ref(|| 0); + + // this is used to force an update specific to test-2 + let magic_rc = use_state(|| 0); + let magic: usize = *magic_rc; + { + let ctx = ctx.clone(); + use_effect(move || { + let count = *rendered.borrow(); + match count { + 0 => { + ctx.set(MyContext("world".into())); + *rendered.borrow_mut() += 1; + } + 1 => { + // force test-2 to re-render. + magic_rc.set(1); + *rendered.borrow_mut() += 1; + } + 2 => { + ctx.set(MyContext("hello world!".into())); + *rendered.borrow_mut() += 1; + } + _ => (), + }; + || {} + }); } + Ok(html! { + + + + + + + }) } - type TestComponent = FunctionComponent; yew::start_app_in_element::( gloo_utils::document().get_element_by_id("output").unwrap(), diff --git a/packages/yew/tests/use_effect.rs b/packages/yew/tests/use_effect.rs index bf064a87064..9b54c3e7e06 100644 --- a/packages/yew/tests/use_effect.rs +++ b/packages/yew/tests/use_effect.rs @@ -4,17 +4,12 @@ use common::obtain_result; use std::ops::{Deref, DerefMut}; use std::rc::Rc; use wasm_bindgen_test::*; -use yew::functional::{ - use_effect_with_deps, use_mut_ref, use_state, FunctionComponent, FunctionProvider, -}; -use yew::{html, HtmlResult, Properties}; +use yew::prelude::*; wasm_bindgen_test::wasm_bindgen_test_configure!(run_in_browser); #[wasm_bindgen_test] fn use_effect_destroys_on_component_drop() { - struct UseEffectFunction {} - struct UseEffectWrapper {} #[derive(Properties, Clone)] struct WrapperProps { destroy_called: Rc, @@ -34,42 +29,37 @@ fn use_effect_destroys_on_component_drop() { false } } - type UseEffectComponent = FunctionComponent; - type UseEffectWrapperComponent = FunctionComponent; - impl FunctionProvider for UseEffectFunction { - type TProps = FunctionProps; - - fn run(props: &Self::TProps) -> HtmlResult { - let effect_called = props.effect_called.clone(); - let destroy_called = props.destroy_called.clone(); - use_effect_with_deps( - move |_| { - effect_called(); - #[allow(clippy::redundant_closure)] // Otherwise there is a build error - move || destroy_called() - }, - (), - ); - Ok(html! {}) - } + + #[function_component(UseEffectComponent)] + fn use_effect_comp(props: &FunctionProps) -> Html { + let effect_called = props.effect_called.clone(); + let destroy_called = props.destroy_called.clone(); + use_effect_with_deps( + move |_| { + effect_called(); + #[allow(clippy::redundant_closure)] // Otherwise there is a build error + move || destroy_called() + }, + (), + ); + Ok(html! {}) } - impl FunctionProvider for UseEffectWrapper { - type TProps = WrapperProps; - - fn run(props: &Self::TProps) -> HtmlResult { - let show = use_state(|| true); - if *show { - let effect_called: Rc = { Rc::new(move || show.set(false)) }; - Ok(html! { - - }) - } else { - Ok(html! { -
{ "EMPTY" }
- }) - } + + #[function_component(UseEffectWrapperComponent)] + fn use_effect_wrapper_comp(props: &WrapperProps) -> Html { + let show = use_state(|| true); + if *show { + let effect_called: Rc = { Rc::new(move || show.set(false)) }; + Ok(html! { + + }) + } else { + Ok(html! { +
{ "EMPTY" }
+ }) } } + let destroy_counter = Rc::new(std::cell::RefCell::new(0)); let destroy_counter_c = destroy_counter.clone(); yew::start_app_with_props_in_element::( @@ -83,35 +73,30 @@ fn use_effect_destroys_on_component_drop() { #[wasm_bindgen_test] fn use_effect_works_many_times() { - struct UseEffectFunction {} - impl FunctionProvider for UseEffectFunction { - type TProps = (); - - fn run(_: &Self::TProps) -> HtmlResult { - let counter = use_state(|| 0); - let counter_clone = counter.clone(); - - use_effect_with_deps( - move |_| { - if *counter_clone < 4 { - counter_clone.set(*counter_clone + 1); - } - || {} - }, - *counter, - ); - - Ok(html! { -
- { "The test result is" } -
{ *counter }
- { "\n" } -
- }) - } + #[function_component(UseEffectComponent)] + fn use_effect_comp() -> Html { + let counter = use_state(|| 0); + let counter_clone = counter.clone(); + + use_effect_with_deps( + move |_| { + if *counter_clone < 4 { + counter_clone.set(*counter_clone + 1); + } + || {} + }, + *counter, + ); + + Ok(html! { +
+ { "The test result is" } +
{ *counter }
+ { "\n" } +
+ }) } - type UseEffectComponent = FunctionComponent; yew::start_app_in_element::( gloo_utils::document().get_element_by_id("output").unwrap(), ); @@ -121,32 +106,28 @@ fn use_effect_works_many_times() { #[wasm_bindgen_test] fn use_effect_works_once() { - struct UseEffectFunction {} - impl FunctionProvider for UseEffectFunction { - type TProps = (); - - fn run(_: &Self::TProps) -> HtmlResult { - let counter = use_state(|| 0); - let counter_clone = counter.clone(); - - use_effect_with_deps( - move |_| { - counter_clone.set(*counter_clone + 1); - || panic!("Destructor should not have been called") - }, - (), - ); - - Ok(html! { -
- { "The test result is" } -
{ *counter }
- { "\n" } -
- }) - } + #[function_component(UseEffectComponent)] + fn use_effect_comp() -> Html { + let counter = use_state(|| 0); + let counter_clone = counter.clone(); + + use_effect_with_deps( + move |_| { + counter_clone.set(*counter_clone + 1); + || panic!("Destructor should not have been called") + }, + (), + ); + + Ok(html! { +
+ { "The test result is" } +
{ *counter }
+ { "\n" } +
+ }) } - type UseEffectComponent = FunctionComponent; + yew::start_app_in_element::( gloo_utils::document().get_element_by_id("output").unwrap(), ); @@ -156,45 +137,41 @@ fn use_effect_works_once() { #[wasm_bindgen_test] fn use_effect_refires_on_dependency_change() { - struct UseEffectFunction {} - impl FunctionProvider for UseEffectFunction { - type TProps = (); - - fn run(_: &Self::TProps) -> HtmlResult { - let number_ref = use_mut_ref(|| 0); - let number_ref_c = number_ref.clone(); - let number_ref2 = use_mut_ref(|| 0); - let number_ref2_c = number_ref2.clone(); - let arg = *number_ref.borrow_mut().deref_mut(); - let counter = use_state(|| 0); - use_effect_with_deps( - move |dep| { - let mut ref_mut = number_ref_c.borrow_mut(); - let inner_ref_mut = ref_mut.deref_mut(); - if *inner_ref_mut < 1 { - *inner_ref_mut += 1; - assert_eq!(dep, &0); - } else { - assert_eq!(dep, &1); - } - counter.set(10); // we just need to make sure it does not panic - move || { - counter.set(11); - *number_ref2_c.borrow_mut().deref_mut() += 1; - } - }, - arg, - ); - Ok(html! { -
- {"The test result is"} -
{*number_ref.borrow_mut().deref_mut()}{*number_ref2.borrow_mut().deref_mut()}
- {"\n"} -
- }) - } + #[function_component(UseEffectComponent)] + fn use_effect_comp() -> Html { + let number_ref = use_mut_ref(|| 0); + let number_ref_c = number_ref.clone(); + let number_ref2 = use_mut_ref(|| 0); + let number_ref2_c = number_ref2.clone(); + let arg = *number_ref.borrow_mut().deref_mut(); + let counter = use_state(|| 0); + use_effect_with_deps( + move |dep| { + let mut ref_mut = number_ref_c.borrow_mut(); + let inner_ref_mut = ref_mut.deref_mut(); + if *inner_ref_mut < 1 { + *inner_ref_mut += 1; + assert_eq!(dep, &0); + } else { + assert_eq!(dep, &1); + } + counter.set(10); // we just need to make sure it does not panic + move || { + counter.set(11); + *number_ref2_c.borrow_mut().deref_mut() += 1; + } + }, + arg, + ); + Ok(html! { +
+ {"The test result is"} +
{*number_ref.borrow_mut().deref_mut()}{*number_ref2.borrow_mut().deref_mut()}
+ {"\n"} +
+ }) } - type UseEffectComponent = FunctionComponent; + yew::start_app_in_element::( gloo_utils::document().get_element_by_id("output").unwrap(), ); diff --git a/packages/yew/tests/use_reducer.rs b/packages/yew/tests/use_reducer.rs index de81203edd6..2a31c3942a9 100644 --- a/packages/yew/tests/use_reducer.rs +++ b/packages/yew/tests/use_reducer.rs @@ -31,30 +31,27 @@ impl Reducible for CounterState { #[wasm_bindgen_test] fn use_reducer_works() { - struct UseReducerFunction {} - impl FunctionProvider for UseReducerFunction { - type TProps = (); - fn run(_: &Self::TProps) -> HtmlResult { - let counter = use_reducer(|| CounterState { counter: 10 }); - - let counter_clone = counter.clone(); - use_effect_with_deps( - move |_| { - counter_clone.dispatch(1); - || {} - }, - (), - ); - Ok(html! { -
- {"The test result is"} -
{counter.counter}
- {"\n"} -
- }) - } + #[function_component(UseReducerComponent)] + fn use_reducer_comp() -> Html { + let counter = use_reducer(|| CounterState { counter: 10 }); + + let counter_clone = counter.clone(); + use_effect_with_deps( + move |_| { + counter_clone.dispatch(1); + || {} + }, + (), + ); + Ok(html! { +
+ {"The test result is"} +
{counter.counter}
+ {"\n"} +
+ }) } - type UseReducerComponent = FunctionComponent; + yew::start_app_in_element::( gloo_utils::document().get_element_by_id("output").unwrap(), ); @@ -80,42 +77,39 @@ impl Reducible for ContentState { #[wasm_bindgen_test] fn use_reducer_eq_works() { - struct UseReducerFunction {} - impl FunctionProvider for UseReducerFunction { - type TProps = (); - fn run(_: &Self::TProps) -> HtmlResult { - let content = use_reducer_eq(|| ContentState { - content: HashSet::default(), - }); - - let render_count = use_mut_ref(|| 0); - - let render_count = { - let mut render_count = render_count.borrow_mut(); - *render_count += 1; - - *render_count - }; - - let add_content_a = { - let content = content.clone(); - Callback::from(move |_| content.dispatch("A".to_string())) - }; - - let add_content_b = Callback::from(move |_| content.dispatch("B".to_string())); - - Ok(html! { - <> -
- {"This component has been rendered: "}{render_count}{" Time(s)."} -
- - - - }) - } + #[function_component(UseReducerComponent)] + fn use_reducer_comp() -> Html { + let content = use_reducer_eq(|| ContentState { + content: HashSet::default(), + }); + + let render_count = use_mut_ref(|| 0); + + let render_count = { + let mut render_count = render_count.borrow_mut(); + *render_count += 1; + + *render_count + }; + + let add_content_a = { + let content = content.clone(); + Callback::from(move |_| content.dispatch("A".to_string())) + }; + + let add_content_b = Callback::from(move |_| content.dispatch("B".to_string())); + + Ok(html! { + <> +
+ {"This component has been rendered: "}{render_count}{" Time(s)."} +
+ + + + }) } - type UseReducerComponent = FunctionComponent; + yew::start_app_in_element::( document().get_element_by_id("output").unwrap(), ); diff --git a/packages/yew/tests/use_ref.rs b/packages/yew/tests/use_ref.rs index c27a0faea61..7f1cdb62dd1 100644 --- a/packages/yew/tests/use_ref.rs +++ b/packages/yew/tests/use_ref.rs @@ -3,34 +3,29 @@ mod common; use common::obtain_result; use std::ops::DerefMut; use wasm_bindgen_test::*; -use yew::functional::{use_mut_ref, use_state, FunctionComponent, FunctionProvider}; -use yew::{html, HtmlResult}; +use yew::prelude::*; wasm_bindgen_test::wasm_bindgen_test_configure!(run_in_browser); #[wasm_bindgen_test] fn use_ref_works() { - struct UseRefFunction {} - impl FunctionProvider for UseRefFunction { - type TProps = (); - - fn run(_: &Self::TProps) -> HtmlResult { - let ref_example = use_mut_ref(|| 0); - *ref_example.borrow_mut().deref_mut() += 1; - let counter = use_state(|| 0); - if *counter < 5 { - counter.set(*counter + 1) - } - Ok(html! { -
- {"The test output is: "} -
{*ref_example.borrow_mut().deref_mut() > 4}
- {"\n"} -
- }) + #[function_component(UseRefComponent)] + fn use_ref_comp() -> Html { + let ref_example = use_mut_ref(|| 0); + *ref_example.borrow_mut().deref_mut() += 1; + let counter = use_state(|| 0); + if *counter < 5 { + counter.set(*counter + 1) } + Ok(html! { +
+ {"The test output is: "} +
{*ref_example.borrow_mut().deref_mut() > 4}
+ {"\n"} +
+ }) } - type UseRefComponent = FunctionComponent; + yew::start_app_in_element::( gloo_utils::document().get_element_by_id("output").unwrap(), ); diff --git a/packages/yew/tests/use_state.rs b/packages/yew/tests/use_state.rs index 78146c6ca4b..7de551338d7 100644 --- a/packages/yew/tests/use_state.rs +++ b/packages/yew/tests/use_state.rs @@ -2,34 +2,27 @@ mod common; use common::obtain_result; use wasm_bindgen_test::*; -use yew::functional::{ - use_effect_with_deps, use_state, use_state_eq, FunctionComponent, FunctionProvider, -}; -use yew::{html, HtmlResult}; +use yew::prelude::*; wasm_bindgen_test::wasm_bindgen_test_configure!(run_in_browser); #[wasm_bindgen_test] fn use_state_works() { - struct UseStateFunction {} - impl FunctionProvider for UseStateFunction { - type TProps = (); - - fn run(_: &Self::TProps) -> HtmlResult { - let counter = use_state(|| 0); - if *counter < 5 { - counter.set(*counter + 1) - } - return Ok(html! { -
- {"Test Output: "} -
{*counter}
- {"\n"} -
- }); + #[function_component(UseStateComponent)] + fn use_state_comp() -> Html { + let counter = use_state(|| 0); + if *counter < 5 { + counter.set(*counter + 1) } + return Ok(html! { +
+ {"Test Output: "} +
{*counter}
+ {"\n"} +
+ }); } - type UseComponent = FunctionComponent; + yew::start_app_in_element::( gloo_utils::document().get_element_by_id("output").unwrap(), ); @@ -39,42 +32,38 @@ fn use_state_works() { #[wasm_bindgen_test] fn multiple_use_state_setters() { - struct UseStateFunction {} - impl FunctionProvider for UseStateFunction { - type TProps = (); - - fn run(_: &Self::TProps) -> HtmlResult { - let counter = use_state(|| 0); - let counter_clone = counter.clone(); - use_effect_with_deps( - move |_| { - // 1st location - counter_clone.set(*counter_clone + 1); - || {} - }, - (), - ); - let another_scope = { - let counter = counter.clone(); - move || { - if *counter < 11 { - // 2nd location - counter.set(*counter + 10) - } + #[function_component(UseStateComponent)] + fn use_state_comp() -> Html { + let counter = use_state(|| 0); + let counter_clone = counter.clone(); + use_effect_with_deps( + move |_| { + // 1st location + counter_clone.set(*counter_clone + 1); + || {} + }, + (), + ); + let another_scope = { + let counter = counter.clone(); + move || { + if *counter < 11 { + // 2nd location + counter.set(*counter + 10) } - }; - another_scope(); - Ok(html! { -
- { "Test Output: " } - // expected output -
{ *counter }
- { "\n" } -
- }) - } + } + }; + another_scope(); + Ok(html! { +
+ { "Test Output: " } + // expected output +
{ *counter }
+ { "\n" } +
+ }) } - type UseComponent = FunctionComponent; + yew::start_app_in_element::( gloo_utils::document().get_element_by_id("output").unwrap(), ); @@ -87,26 +76,21 @@ fn use_state_eq_works() { use std::sync::atomic::{AtomicUsize, Ordering}; static RENDER_COUNT: AtomicUsize = AtomicUsize::new(0); - struct UseStateFunction {} - - impl FunctionProvider for UseStateFunction { - type TProps = (); - - fn run(_: &Self::TProps) -> HtmlResult { - RENDER_COUNT.fetch_add(1, Ordering::Relaxed); - let counter = use_state_eq(|| 0); - counter.set(1); + #[function_component(UseStateComponent)] + fn use_state_comp() -> Html { + RENDER_COUNT.fetch_add(1, Ordering::Relaxed); + let counter = use_state_eq(|| 0); + counter.set(1); - Ok(html! { -
- {"Test Output: "} -
{*counter}
- {"\n"} -
- }) - } + Ok(html! { +
+ {"Test Output: "} +
{*counter}
+ {"\n"} +
+ }) } - type UseComponent = FunctionComponent; + yew::start_app_in_element::( gloo_utils::document().get_element_by_id("output").unwrap(), ); From a87b2c77422df7ca03dd7ae74311470f2e6f6677 Mon Sep 17 00:00:00 2001 From: Kaede Hoshikawa Date: Sat, 22 Jan 2022 17:50:16 +0900 Subject: [PATCH 10/42] Fix tests. --- packages/yew/tests/use_context.rs | 38 +++++++++++++++---------------- packages/yew/tests/use_effect.rs | 22 +++++++++--------- packages/yew/tests/use_reducer.rs | 8 +++---- packages/yew/tests/use_ref.rs | 4 ++-- packages/yew/tests/use_state.rs | 18 +++++++-------- 5 files changed, 45 insertions(+), 45 deletions(-) diff --git a/packages/yew/tests/use_context.rs b/packages/yew/tests/use_context.rs index 1ed7d73ae82..f9b942f54a8 100644 --- a/packages/yew/tests/use_context.rs +++ b/packages/yew/tests/use_context.rs @@ -20,15 +20,15 @@ fn use_context_scoping_works() { use_context::().unwrap() ); }; - Ok(html! { + html! {
- }) + } } #[function_component] fn UseContextComponent() -> Html { type ExampleContextProvider = ContextProvider; - Ok(html! { + html! {
{"ignored"}
@@ -46,15 +46,15 @@ fn use_context_scoping_works() {
- }) + } } #[function_component] fn UseContextComponentInner() -> Html { let context = use_context::(); - Ok(html! { + html! {
{ &context.unwrap().0 }
- }) + } } yew::start_app_in_element::( @@ -73,10 +73,10 @@ fn use_context_works_with_multiple_types() { #[function_component] fn Test1() -> Html { - assert_eq!(use_context::(), Some(ContextA(0))); + assert_eq!(use_context::(), Some(ContextA(2))); assert_eq!(use_context::(), Some(ContextB(1))); - Ok(html! {}) + html! {} } #[function_component] @@ -84,7 +84,7 @@ fn use_context_works_with_multiple_types() { assert_eq!(use_context::(), Some(ContextA(0))); assert_eq!(use_context::(), Some(ContextB(1))); - Ok(html! {}) + html! {} } #[function_component] @@ -92,7 +92,7 @@ fn use_context_works_with_multiple_types() { assert_eq!(use_context::(), Some(ContextA(0))); assert_eq!(use_context::(), None); - Ok(html! {}) + html! {} } #[function_component] @@ -100,7 +100,7 @@ fn use_context_works_with_multiple_types() { assert_eq!(use_context::(), None); assert_eq!(use_context::(), None); - Ok(html! {}) + html! {} } #[function_component] @@ -108,7 +108,7 @@ fn use_context_works_with_multiple_types() { type ContextAProvider = ContextProvider; type ContextBProvider = ContextProvider; - Ok(html! { + html! {
@@ -121,7 +121,7 @@ fn use_context_works_with_multiple_types() {
- }) + } } yew::start_app_in_element::( @@ -144,14 +144,14 @@ fn use_context_update_works() { fn RenderCounter(props: &RenderCounterProps) -> Html { let counter = use_mut_ref(|| 0); *counter.borrow_mut() += 1; - Ok(html! { + html! { <>
{ format!("total: {}", counter.borrow()) }
{ props.children.clone() } - }) + } } #[derive(Clone, Debug, PartialEq, Properties)] @@ -168,14 +168,14 @@ fn use_context_update_works() { let ctx = use_context::>().expect("context not passed down"); - Ok(html! { + html! { <>
{ format!("magic: {}\n", props.magic) }
{ format!("current: {}, total: {}", ctx.0, counter.borrow()) }
- }) + } } #[function_component] @@ -211,14 +211,14 @@ fn use_context_update_works() { || {} }); } - Ok(html! { + html! { - }) + } } yew::start_app_in_element::( diff --git a/packages/yew/tests/use_effect.rs b/packages/yew/tests/use_effect.rs index 9b54c3e7e06..594d607dcb7 100644 --- a/packages/yew/tests/use_effect.rs +++ b/packages/yew/tests/use_effect.rs @@ -42,7 +42,7 @@ fn use_effect_destroys_on_component_drop() { }, (), ); - Ok(html! {}) + html! {} } #[function_component(UseEffectWrapperComponent)] @@ -50,13 +50,13 @@ fn use_effect_destroys_on_component_drop() { let show = use_state(|| true); if *show { let effect_called: Rc = { Rc::new(move || show.set(false)) }; - Ok(html! { + html! { - }) + } } else { - Ok(html! { + html! {
{ "EMPTY" }
- }) + } } } @@ -88,13 +88,13 @@ fn use_effect_works_many_times() { *counter, ); - Ok(html! { + html! {
{ "The test result is" }
{ *counter }
{ "\n" }
- }) + } } yew::start_app_in_element::( @@ -119,13 +119,13 @@ fn use_effect_works_once() { (), ); - Ok(html! { + html! {
{ "The test result is" }
{ *counter }
{ "\n" }
- }) + } } yew::start_app_in_element::( @@ -163,13 +163,13 @@ fn use_effect_refires_on_dependency_change() { }, arg, ); - Ok(html! { + html! {
{"The test result is"}
{*number_ref.borrow_mut().deref_mut()}{*number_ref2.borrow_mut().deref_mut()}
{"\n"}
- }) + } } yew::start_app_in_element::( diff --git a/packages/yew/tests/use_reducer.rs b/packages/yew/tests/use_reducer.rs index 2a31c3942a9..2d6454bc735 100644 --- a/packages/yew/tests/use_reducer.rs +++ b/packages/yew/tests/use_reducer.rs @@ -43,13 +43,13 @@ fn use_reducer_works() { }, (), ); - Ok(html! { + html! {
{"The test result is"}
{counter.counter}
{"\n"}
- }) + } } yew::start_app_in_element::( @@ -99,7 +99,7 @@ fn use_reducer_eq_works() { let add_content_b = Callback::from(move |_| content.dispatch("B".to_string())); - Ok(html! { + html! { <>
{"This component has been rendered: "}{render_count}{" Time(s)."} @@ -107,7 +107,7 @@ fn use_reducer_eq_works() { - }) + } } yew::start_app_in_element::( diff --git a/packages/yew/tests/use_ref.rs b/packages/yew/tests/use_ref.rs index 7f1cdb62dd1..662f09206a6 100644 --- a/packages/yew/tests/use_ref.rs +++ b/packages/yew/tests/use_ref.rs @@ -17,13 +17,13 @@ fn use_ref_works() { if *counter < 5 { counter.set(*counter + 1) } - Ok(html! { + html! {
{"The test output is: "}
{*ref_example.borrow_mut().deref_mut() > 4}
{"\n"}
- }) + } } yew::start_app_in_element::( diff --git a/packages/yew/tests/use_state.rs b/packages/yew/tests/use_state.rs index 7de551338d7..1dd59b1de69 100644 --- a/packages/yew/tests/use_state.rs +++ b/packages/yew/tests/use_state.rs @@ -8,19 +8,19 @@ wasm_bindgen_test::wasm_bindgen_test_configure!(run_in_browser); #[wasm_bindgen_test] fn use_state_works() { - #[function_component(UseStateComponent)] + #[function_component(UseComponent)] fn use_state_comp() -> Html { let counter = use_state(|| 0); if *counter < 5 { counter.set(*counter + 1) } - return Ok(html! { + html! {
{"Test Output: "}
{*counter}
{"\n"}
- }); + } } yew::start_app_in_element::( @@ -32,7 +32,7 @@ fn use_state_works() { #[wasm_bindgen_test] fn multiple_use_state_setters() { - #[function_component(UseStateComponent)] + #[function_component(UseComponent)] fn use_state_comp() -> Html { let counter = use_state(|| 0); let counter_clone = counter.clone(); @@ -54,14 +54,14 @@ fn multiple_use_state_setters() { } }; another_scope(); - Ok(html! { + html! {
{ "Test Output: " } // expected output
{ *counter }
{ "\n" }
- }) + } } yew::start_app_in_element::( @@ -76,19 +76,19 @@ fn use_state_eq_works() { use std::sync::atomic::{AtomicUsize, Ordering}; static RENDER_COUNT: AtomicUsize = AtomicUsize::new(0); - #[function_component(UseStateComponent)] + #[function_component(UseComponent)] fn use_state_comp() -> Html { RENDER_COUNT.fetch_add(1, Ordering::Relaxed); let counter = use_state_eq(|| 0); counter.set(1); - Ok(html! { + html! {
{"Test Output: "}
{*counter}
{"\n"}
- }) + } } yew::start_app_in_element::( From 62396f0ae0abe436a3a552c8a9dcd1957cc2b822 Mon Sep 17 00:00:00 2001 From: Kaede Hoshikawa Date: Sat, 22 Jan 2022 18:37:48 +0900 Subject: [PATCH 11/42] Migrate to macro-based hooks. --- .../src/hooks/use_bool_toggle.rs | 3 +- examples/simple_ssr/src/main.rs | 1 + examples/suspense/src/use_sleep.rs | 1 + packages/yew-agent/src/hooks.rs | 1 + packages/yew-macro/src/function_component.rs | 21 ++- packages/yew-macro/src/hook/body.rs | 4 +- packages/yew-macro/src/hook/lifetime.rs | 18 +-- packages/yew-macro/src/hook/mod.rs | 27 ++-- packages/yew-router/src/hooks.rs | 3 + packages/yew/Cargo.toml | 2 - packages/yew/src/functional/hooks/mod.rs | 50 +----- .../yew/src/functional/hooks/use_context.rs | 69 +-------- .../yew/src/functional/hooks/use_effect.rs | 125 +-------------- .../yew/src/functional/hooks/use_reducer.rs | 144 +----------------- packages/yew/src/functional/hooks/use_ref.rs | 144 ++---------------- .../yew/src/functional/hooks/use_state.rs | 63 +------- packages/yew/src/functional/mod.rs | 44 ++---- packages/yew/tests/mod.rs | 22 ++- packages/yew/tests/suspense.rs | 4 + packages/yew/tests/use_context.rs | 32 ++-- 20 files changed, 117 insertions(+), 661 deletions(-) diff --git a/examples/function_todomvc/src/hooks/use_bool_toggle.rs b/examples/function_todomvc/src/hooks/use_bool_toggle.rs index 221fe27f363..d8281c1e7b1 100644 --- a/examples/function_todomvc/src/hooks/use_bool_toggle.rs +++ b/examples/function_todomvc/src/hooks/use_bool_toggle.rs @@ -1,6 +1,6 @@ use std::ops::Deref; use std::rc::Rc; -use yew::{use_state_eq, UseStateHandle}; +use yew::prelude::*; #[derive(Clone)] pub struct UseBoolToggleHandle { @@ -47,6 +47,7 @@ impl Deref for UseBoolToggleHandle { /// /// ... /// ``` +#[hook] pub fn use_bool_toggle(default: bool) -> UseBoolToggleHandle { let state = use_state_eq(|| default); diff --git a/examples/simple_ssr/src/main.rs b/examples/simple_ssr/src/main.rs index 96a29286ef7..58dbb0dda8d 100644 --- a/examples/simple_ssr/src/main.rs +++ b/examples/simple_ssr/src/main.rs @@ -57,6 +57,7 @@ impl PartialEq for UuidState { } } +#[hook] fn use_random_uuid() -> SuspensionResult { let s = use_state(UuidState::new); diff --git a/examples/suspense/src/use_sleep.rs b/examples/suspense/src/use_sleep.rs index e8e8ff648ce..98bf4e60f12 100644 --- a/examples/suspense/src/use_sleep.rs +++ b/examples/suspense/src/use_sleep.rs @@ -28,6 +28,7 @@ impl Reducible for SleepState { } } +#[hook] pub fn use_sleep() -> SuspensionResult> { let sleep_state = use_reducer(SleepState::new); diff --git a/packages/yew-agent/src/hooks.rs b/packages/yew-agent/src/hooks.rs index 74c5bd4b65d..a54324c2461 100644 --- a/packages/yew-agent/src/hooks.rs +++ b/packages/yew-agent/src/hooks.rs @@ -29,6 +29,7 @@ where /// /// Takes a callback as the only argument. The callback will be updated on every render to make /// sure captured values (if any) are up to date. +#[hook] pub fn use_bridge(on_output: F) -> UseBridgeHandle where T: Bridged, diff --git a/packages/yew-macro/src/function_component.rs b/packages/yew-macro/src/function_component.rs index e7786a103aa..d2b6c21d340 100644 --- a/packages/yew-macro/src/function_component.rs +++ b/packages/yew-macro/src/function_component.rs @@ -3,7 +3,11 @@ use quote::{format_ident, quote, ToTokens}; use syn::parse::{Parse, ParseStream}; use syn::punctuated::Punctuated; use syn::token::{Comma, Fn}; -use syn::{Attribute, Block, FnArg, Generics, Ident, Item, ItemFn, ReturnType, Type, Visibility}; +use syn::{ + visit_mut, Attribute, Block, FnArg, Generics, Ident, Item, ItemFn, ReturnType, Type, Visibility, +}; + +use crate::hook::BodyRewriter; #[derive(Clone)] pub struct FunctionComponent { @@ -169,7 +173,7 @@ fn print_fn(func_comp: FunctionComponent, use_fn_name: bool) -> TokenStream { fn_token, name, attrs, - block, + mut block, return_type, generics, arg, @@ -184,9 +188,14 @@ fn print_fn(func_comp: FunctionComponent, use_fn_name: bool) -> TokenStream { Ident::new("inner", Span::mixed_site()) }; + let ctx_ident = Ident::new("ctx", Span::mixed_site()); + + let mut body_rewriter = BodyRewriter::default(); + visit_mut::visit_block_mut(&mut body_rewriter, &mut *block); + quote! { #(#attrs)* - #fn_token #name #ty_generics (#arg) -> #return_type + #fn_token #name #ty_generics (#ctx_ident: &mut ::yew::functional::HookContext, #arg) -> #return_type #where_clause { #block @@ -241,6 +250,8 @@ pub fn function_component_impl( Ident::new("inner", Span::mixed_site()) }; + let ctx_ident = Ident::new("ctx", Span::mixed_site()); + let quoted = quote! { #[doc(hidden)] #[allow(non_camel_case_types)] @@ -253,10 +264,10 @@ pub fn function_component_impl( impl #impl_generics ::yew::functional::FunctionProvider for #provider_name #ty_generics #where_clause { type TProps = #props_type; - fn run(#provider_props: &Self::TProps) -> ::yew::html::HtmlResult { + fn run(#ctx_ident: &mut ::yew::functional::HookContext, #provider_props: &Self::TProps) -> ::yew::html::HtmlResult { #func - ::yew::html::IntoHtmlResult::into_html_result(#fn_name #fn_generics (#provider_props)) + ::yew::html::IntoHtmlResult::into_html_result(#fn_name #fn_generics (#ctx_ident, #provider_props)) } } diff --git a/packages/yew-macro/src/hook/body.rs b/packages/yew-macro/src/hook/body.rs index f6b7fffdf64..ee01930d2e6 100644 --- a/packages/yew-macro/src/hook/body.rs +++ b/packages/yew-macro/src/hook/body.rs @@ -30,7 +30,7 @@ impl BodyRewriter { impl VisitMut for BodyRewriter { fn visit_expr_call_mut(&mut self, i: &mut ExprCall) { - let states_ident = Ident::new("states", Span::mixed_site()); + let ctx_ident = Ident::new("ctx", Span::mixed_site()); // Only rewrite hook calls. if let Expr::Path(ref m) = &*i.func { @@ -39,7 +39,7 @@ impl VisitMut for BodyRewriter { if self.is_branched() { emit_error!(m, "hooks cannot be called at this position."); } else { - *i = parse_quote_spanned! { i.span() => ::yew::functional::Hook::run(#i, #states_ident) }; + *i = parse_quote_spanned! { i.span() => ::yew::functional::Hook::run(#i, #ctx_ident) }; } return; diff --git a/packages/yew-macro/src/hook/lifetime.rs b/packages/yew-macro/src/hook/lifetime.rs index fa2eaaa39db..7f108c7f157 100644 --- a/packages/yew-macro/src/hook/lifetime.rs +++ b/packages/yew-macro/src/hook/lifetime.rs @@ -2,13 +2,12 @@ use proc_macro2::Span; use syn::visit_mut::{self, VisitMut}; use syn::{GenericArgument, Lifetime, Receiver, TypeReference}; -// borrowed from the async-trait crate. +// borrowed from the awesome async-trait crate. pub struct CollectLifetimes { pub elided: Vec, pub explicit: Vec, pub name: &'static str, pub default_span: Span, - // pub fn_ctr: u64, } impl CollectLifetimes { @@ -18,7 +17,6 @@ impl CollectLifetimes { explicit: Vec::new(), name, default_span, - // fn_ctr: 0, } } @@ -44,20 +42,6 @@ impl CollectLifetimes { self.elided.push(life.clone()); life } - - // fn is_in_fn(&self) -> bool { - // self.fn_ctr > 0 - // } - - // fn with_in_fn(&mut self, f: F) -> O - // where - // F: FnOnce(&mut CollectLifetimes) -> O, - // { - // self.fn_ctr += 1; - // let result = { f(self) }; - // self.fn_ctr -= 1; - // result - // } } impl VisitMut for CollectLifetimes { diff --git a/packages/yew-macro/src/hook/mod.rs b/packages/yew-macro/src/hook/mod.rs index da190ee43ba..6a038bb6493 100644 --- a/packages/yew-macro/src/hook/mod.rs +++ b/packages/yew-macro/src/hook/mod.rs @@ -10,6 +10,7 @@ mod body; mod lifetime; mod signature; +pub use body::BodyRewriter; use signature::HookSignature; #[derive(Clone)] @@ -91,12 +92,12 @@ When used in function components and hooks, this hook is equivalent to: let (impl_generics, ty_generics, where_clause) = generics.split_for_impl(); let call_generics = ty_generics.as_turbofish(); - let states = Ident::new("states", Span::mixed_site()); + let ctx_ident = Ident::new("ctx", Span::mixed_site()); let phantom_types = hook_sig.phantom_types(); let phantom_lifetimes = hook_sig.phantom_lifetimes(); - let mut body_rewriter = body::BodyRewriter::default(); + let mut body_rewriter = BodyRewriter::default(); visit_mut::visit_block_mut(&mut body_rewriter, &mut *block); let hook_lifetime_plus = hook_lifetime.map(|m| quote! { #m + }); @@ -104,27 +105,25 @@ When used in function components and hooks, this hook is equivalent to: // let inner_fn_ident = Ident::new("inner_fn", Span::mixed_site()); // let input_args = hook_sig.input_args(); - let boxed_fn_type = { - let rt = match &sig.output { - ReturnType::Default => None, - ReturnType::Type(_, _) => Some(quote! { -> #output_type }), - }; - quote! { ::std::boxed::Box } + let boxed_fn_rt = match &sig.output { + ReturnType::Default => None, + ReturnType::Type(_, _) => Some(quote! { -> #output_type }), }; + let boxed_fn_type = quote! { ::std::boxed::Box }; let output = quote! { #[doc = #doc_text] #(#attrs)* #vis #fn_token #ident #generics (#inputs) #hook_return_type #where_clause { - // fn #inner_fn_ident #generics (#states: &mut ::yew::functional::HookStates, #inputs) -> #output_type #block + // fn #inner_fn_ident #generics (#ctx_ident: &mut ::yew::functional::HookContext, #inputs) -> #output_type #block // always capture inputs with closure for now, we need boxing implementation for `impl Trait` // arguments anyways. - // let inner = ::std::boxed::Box::new(move |#states: &mut ::yew::functional::HookStates| #inner_fn_ident #call_generics (#states, #(#input_args)*) ) - // as ::std::boxed::Box<#hook_lifetime_plus FnOnce(&mut ::yew::functional::HookStates) -> #output_type>; + // let inner = ::std::boxed::Box::new(move |#ctx_ident: &mut ::yew::functional::HookContext| #inner_fn_ident #call_generics (#ctx_ident, #(#input_args)*) ) + // as ::std::boxed::Box<#hook_lifetime_plus FnOnce(&mut ::yew::functional::HookContext) -> #output_type>; - let #inner_ident = ::std::boxed::Box::new(move |#states: &mut ::yew::functional::HookStates| #block ) + let #inner_ident = ::std::boxed::Box::new(move |#ctx_ident: &mut ::yew::functional::HookContext| #boxed_fn_rt #block ) as #boxed_fn_type; struct #hook_struct_name #generics #where_clause { @@ -135,8 +134,8 @@ When used in function components and hooks, this hook is equivalent to: impl #impl_generics ::yew::functional::Hook for #hook_struct_name #ty_generics #where_clause { type Output = #output_type; - fn run(mut self, #states: &mut ::yew::functional::HookStates) -> Self::Output { - (self.inner)(#states) + fn run(mut self, #ctx_ident: &mut ::yew::functional::HookContext) -> Self::Output { + (self.inner)(#ctx_ident) } } diff --git a/packages/yew-router/src/hooks.rs b/packages/yew-router/src/hooks.rs index 11828bd8403..89c7585b348 100644 --- a/packages/yew-router/src/hooks.rs +++ b/packages/yew-router/src/hooks.rs @@ -8,11 +8,13 @@ use crate::router::{LocationContext, NavigatorContext}; use yew::prelude::*; /// A hook to access the [`Navigator`]. +#[hook] pub fn use_navigator() -> Option { use_context::().map(|m| m.navigator()) } /// A hook to access the current [`Location`]. +#[hook] pub fn use_location() -> Option { Some(use_context::()?.location()) } @@ -25,6 +27,7 @@ pub fn use_location() -> Option { /// /// If your `Routable` has a `#[not_found]` route, you can use `.unwrap_or_default()` instead of /// `.unwrap()` to unwrap. +#[hook] pub fn use_route() -> Option where R: Routable + 'static, diff --git a/packages/yew/Cargo.toml b/packages/yew/Cargo.toml index 9dee3e2759f..fa63c9d84d7 100644 --- a/packages/yew/Cargo.toml +++ b/packages/yew/Cargo.toml @@ -27,8 +27,6 @@ wasm-bindgen = "0.2" yew-macro = { version = "^0.19.0", path = "../yew-macro" } thiserror = "1.0" -scoped-tls-hkt = "0.1" - futures = { version = "0.3", optional = true } html-escape = { version = "0.2.9", optional = true } diff --git a/packages/yew/src/functional/hooks/mod.rs b/packages/yew/src/functional/hooks/mod.rs index 6bb81a1f6ec..11d1af1982d 100644 --- a/packages/yew/src/functional/hooks/mod.rs +++ b/packages/yew/src/functional/hooks/mod.rs @@ -10,7 +10,7 @@ pub use use_reducer::*; pub use use_ref::*; pub use use_state::*; -use crate::functional::{AnyScope, HookStates, HookUpdater, CURRENT_HOOK}; +use crate::functional::{AnyScope, HookContext, HookUpdater}; /// A trait that is implemented on hooks. /// @@ -21,7 +21,7 @@ pub trait Hook { type Output; /// Runs the hook inside current state, returns output upon completion. - fn run(self, states: &mut HookStates) -> Self::Output; + fn run(self, ctx: &mut HookContext) -> Self::Output; } /// Low level building block of creating hooks. @@ -33,43 +33,7 @@ pub trait Hook { /// The `initializer` callback is called once to create the initial state of the hook. /// `runner` callback handles the logic of the hook. It is called when the hook function is called. /// `destructor`, as the name implies, is called to cleanup the leftovers of the hook. -/// -/// See the pre-defined hooks for examples of how to use this function. -/// -/// [Yew Docs]: https://yew.rs/next/concepts/function-components/custom-hooks -pub fn use_hook( - initializer: impl FnOnce() -> InternalHook, - runner: impl FnOnce(&mut InternalHook, HookUpdater) -> Output, - destructor: Tear, -) -> Output { - if !CURRENT_HOOK.is_set() { - panic!("Hooks can only be used in the scope of a function component"); - } - - // Extract current hook - let updater = CURRENT_HOOK - .with(|hook_state| hook_state.next_state::(initializer, destructor)); - - // Execute the actual hook closure we were given. Let it mutate the hook state and let - // it create a callback that takes the mutable hook state. - let mut hook = updater.hook.borrow_mut(); - let hook: &mut InternalHook = hook - .downcast_mut() - .expect("Incompatible hook type. Hooks must always be called in the same order"); - - runner(hook, updater.clone()) -} - -/// Low level building block of creating hooks. -/// -/// It is used to created the pre-defined primitive hooks. -/// Generally, it isn't needed to create hooks and should be avoided as most custom hooks can be -/// created by combining other hooks as described in [Yew Docs]. -/// -/// The `initializer` callback is called once to create the initial state of the hook. -/// `runner` callback handles the logic of the hook. It is called when the hook function is called. -/// `destructor`, as the name implies, is called to cleanup the leftovers of the hook. -pub(crate) fn use_hook_next<'hook, T, INIT, RUN, TEAR, O>( +pub(crate) fn use_hook<'hook, T, INIT, RUN, TEAR, O>( initializer: INIT, runner: RUN, destructor: TEAR, @@ -93,7 +57,7 @@ where { type Output = O; - fn run(self, states: &mut HookStates) -> Self::Output { + fn run(self, ctx: &mut HookContext) -> Self::Output { let Self { initializer, runner, @@ -101,7 +65,7 @@ where } = self; // Extract current hook - let updater = states.next_state::(initializer, destructor); + let updater = ctx.next_state::(initializer, destructor); // Execute the actual hook closure we were given. Let it mutate the hook state and let // it create a callback that takes the mutable hook state. @@ -127,8 +91,8 @@ pub(crate) fn use_component_scope() -> impl Hook { impl Hook for HookProvider { type Output = AnyScope; - fn run(self, states: &mut HookStates) -> Self::Output { - states.scope().clone() + fn run(self, ctx: &mut HookContext) -> Self::Output { + ctx.scope().clone() } } diff --git a/packages/yew/src/functional/hooks/use_context.rs b/packages/yew/src/functional/hooks/use_context.rs index 223ab583831..d082e1dace0 100644 --- a/packages/yew/src/functional/hooks/use_context.rs +++ b/packages/yew/src/functional/hooks/use_context.rs @@ -1,68 +1,5 @@ use crate::context::ContextHandle; -use crate::functional::{get_current_scope, hook, use_component_scope, use_hook, use_hook_next}; - -/// Hook for consuming context values in function components. -/// The context of the type passed as `T` is returned. If there is no such context in scope, `None` is returned. -/// A component which calls `use_context` will re-render when the data of the context changes. -/// -/// More information about contexts and how to define and consume them can be found on [Yew Docs](https://yew.rs). -/// -/// # Example -/// ```rust -/// # use yew::prelude::*; -/// # use std::rc::Rc; -/// -/// # #[derive(Clone, Debug, PartialEq)] -/// # struct ThemeContext { -/// # foreground: String, -/// # background: String, -/// # } -/// #[function_component(ThemedButton)] -/// pub fn themed_button() -> Html { -/// let theme = use_context::>().expect("no ctx found"); -/// -/// html! { -/// -/// } -/// } -/// ``` -pub fn use_context() -> Option { - struct UseContextState { - initialized: bool, - context: Option<(T2, ContextHandle)>, - } - - let scope = get_current_scope() - .expect("No current Scope. `use_context` can only be called inside function components"); - - use_hook( - move || UseContextState { - initialized: false, - context: None, - }, - |state: &mut UseContextState, updater| { - if !state.initialized { - state.initialized = true; - let callback = move |ctx: T| { - updater.callback(|state: &mut UseContextState| { - if let Some(context) = &mut state.context { - context.0 = ctx; - } - true - }); - }; - state.context = scope.context::(callback.into()); - } - - Some(state.context.as_ref()?.0.clone()) - }, - |state| { - state.context = None; - }, - ) -} +use crate::functional::{hook, use_component_scope, use_hook}; /// Hook for consuming context values in function components. /// The context of the type passed as `T` is returned. If there is no such context in scope, `None` is returned. @@ -92,7 +29,7 @@ pub fn use_context() -> Option { /// } /// ``` #[hook] -pub fn use_context_next() -> Option { +pub fn use_context() -> Option { struct UseContextState { initialized: bool, context: Option<(T2, ContextHandle)>, @@ -100,7 +37,7 @@ pub fn use_context_next() -> Option { let scope = use_component_scope(); - use_hook_next( + use_hook( move || UseContextState { initialized: false, context: None, diff --git a/packages/yew/src/functional/hooks/use_effect.rs b/packages/yew/src/functional/hooks/use_effect.rs index 08a5967b783..15cf5295dcf 100644 --- a/packages/yew/src/functional/hooks/use_effect.rs +++ b/packages/yew/src/functional/hooks/use_effect.rs @@ -1,4 +1,4 @@ -use crate::functional::{hook, use_hook, use_hook_next}; +use crate::functional::{hook, use_hook}; use std::rc::Rc; struct UseEffect { @@ -36,6 +36,7 @@ struct UseEffect { /// } /// } /// ``` +#[hook] pub fn use_effect(callback: impl FnOnce() -> Destructor + 'static) where Destructor: FnOnce() + 'static, @@ -72,73 +73,6 @@ where ) } -/// This hook is used for hooking into the component's lifecycle. -/// -/// # Example -/// ```rust -/// # use yew::prelude::*; -/// # use std::rc::Rc; -/// # -/// #[function_component(UseEffect)] -/// fn effect() -> Html { -/// let counter = use_state(|| 0); -/// -/// let counter_one = counter.clone(); -/// use_effect(move || { -/// // Make a call to DOM API after component is rendered -/// gloo_utils::document().set_title(&format!("You clicked {} times", *counter_one)); -/// -/// // Perform the cleanup -/// || gloo_utils::document().set_title(&format!("You clicked 0 times")) -/// }); -/// -/// let onclick = { -/// let counter = counter.clone(); -/// Callback::from(move |_| counter.set(*counter + 1)) -/// }; -/// -/// html! { -/// -/// } -/// } -/// ``` -#[hook] -pub fn use_effect_next(callback: impl FnOnce() -> Destructor + 'static) -where - Destructor: FnOnce() + 'static, -{ - use_hook_next( - move || { - let effect: UseEffect = UseEffect { - runner: None, - destructor: None, - }; - effect - }, - |state, updater| { - state.runner = Some(Box::new(callback) as Box Destructor>); - - // Run on every render - updater.post_render(move |state: &mut UseEffect| { - if let Some(callback) = state.runner.take() { - if let Some(de) = state.destructor.take() { - de(); - } - - let new_destructor = callback(); - state.destructor.replace(Box::new(new_destructor)); - } - false - }); - }, - |hook| { - if let Some(destructor) = hook.destructor.take() { - destructor() - } - }, - ) -} - type UseEffectDepsRunnerFn = Box Destructor>; struct UseEffectDeps { @@ -155,6 +89,7 @@ struct UseEffectDeps { /// Whenever the dependencies are changed, the effect callback is called again. /// To detect changes, dependencies must implement `PartialEq`. /// Note that the destructor also runs when dependencies change. +#[hook] pub fn use_effect_with_deps(callback: Callback, deps: Dependents) where Callback: FnOnce(&Dependents) -> Destructor + 'static, @@ -200,57 +135,3 @@ where }, ); } - -/// This hook is similar to [`use_effect`] but it accepts dependencies. -/// -/// Whenever the dependencies are changed, the effect callback is called again. -/// To detect changes, dependencies must implement `PartialEq`. -/// Note that the destructor also runs when dependencies change. -#[hook] -pub fn use_effect_with_deps_next( - callback: Callback, - deps: Dependents, -) where - Callback: FnOnce(&Dependents) -> Destructor + 'static, - Destructor: FnOnce() + 'static, - Dependents: PartialEq + 'static, -{ - let deps = Rc::new(deps); - - use_hook_next( - move || { - let destructor: Option> = None; - UseEffectDeps { - runner_with_deps: None, - destructor, - deps: None, - } - }, - move |state, updater| { - state.runner_with_deps = Some((deps, Box::new(callback))); - - updater.post_render(move |state: &mut UseEffectDeps| { - if let Some((deps, callback)) = state.runner_with_deps.take() { - if Some(&deps) == state.deps.as_ref() { - return false; - } - - if let Some(de) = state.destructor.take() { - de(); - } - - let new_destructor = callback(&deps); - - state.deps = Some(deps); - state.destructor = Some(Box::new(new_destructor)); - } - false - }); - }, - |hook| { - if let Some(destructor) = hook.destructor.take() { - destructor() - } - }, - ); -} diff --git a/packages/yew/src/functional/hooks/use_reducer.rs b/packages/yew/src/functional/hooks/use_reducer.rs index 847c573e24e..60d867d77d3 100644 --- a/packages/yew/src/functional/hooks/use_reducer.rs +++ b/packages/yew/src/functional/hooks/use_reducer.rs @@ -3,7 +3,7 @@ use std::fmt; use std::ops::Deref; use std::rc::Rc; -use crate::functional::{hook, use_hook, use_hook_next}; +use crate::functional::{hook, use_hook}; type DispatchFn = Rc::Action)>; @@ -144,6 +144,7 @@ where } /// The base function of [`use_reducer`] and [`use_reducer_eq`] +#[hook] fn use_reducer_base(initial_fn: F, should_render_fn: R) -> UseReducerHandle where T: Reducible + 'static, @@ -191,55 +192,6 @@ where ) } -/// The base function of [`use_reducer`] and [`use_reducer_eq`] -#[hook] -fn use_reducer_base_next(initial_fn: F, should_render_fn: R) -> UseReducerHandle -where - T: Reducible + 'static, - F: FnOnce() -> T, - R: (Fn(&T, &T) -> bool) + 'static, -{ - use_hook_next( - move || UseReducer { - current_state: Rc::new(initial_fn()), - dispatch: RefCell::default(), - }, - |s, updater| { - let mut dispatch_ref = s.dispatch.borrow_mut(); - - // Create dispatch once. - let dispatch = match *dispatch_ref { - Some(ref m) => (*m).to_owned(), - None => { - let should_render_fn = Rc::new(should_render_fn); - - let dispatch: Rc = Rc::new(move |action: T::Action| { - let should_render_fn = should_render_fn.clone(); - - updater.callback(move |state: &mut UseReducer| { - let next_state = state.current_state.clone().reduce(action); - let should_render = should_render_fn(&next_state, &state.current_state); - state.current_state = next_state; - - should_render - }); - }); - - *dispatch_ref = Some(dispatch.clone()); - - dispatch - } - }; - - UseReducerHandle { - value: Rc::clone(&s.current_state), - dispatch, - } - }, - |_| {}, - ) -} - /// This hook is an alternative to [`use_state`](super::use_state()). /// It is used to handle component's state and is used when complex actions needs to be performed on said state. /// @@ -308,6 +260,7 @@ where /// } /// } /// ``` +#[hook] pub fn use_reducer(initial_fn: F) -> UseReducerHandle where T: Reducible + 'static, @@ -320,6 +273,7 @@ where /// /// This requires the state to implement [`PartialEq`] in addition to the [`Reducible`] trait /// required by [`use_reducer`]. +#[hook] pub fn use_reducer_eq(initial_fn: F) -> UseReducerHandle where T: Reducible + PartialEq + 'static, @@ -327,93 +281,3 @@ where { use_reducer_base(initial_fn, T::ne) } - -/// This hook is an alternative to [`use_state`](super::use_state()). -/// It is used to handle component's state and is used when complex actions needs to be performed on said state. -/// -/// The state is expected to implement the [`Reducible`] trait which provides an `Action` type and a reducer -/// function. -/// -/// # Example -/// ```rust -/// # use yew::prelude::*; -/// # use std::rc::Rc; -/// # -/// -/// /// reducer's Action -/// enum CounterAction { -/// Double, -/// Square, -/// } -/// -/// /// reducer's State -/// struct CounterState { -/// counter: i32, -/// } -/// -/// impl Default for CounterState { -/// fn default() -> Self { -/// Self { counter: 1 } -/// } -/// } -/// -/// impl Reducible for CounterState { -/// /// Reducer Action Type -/// type Action = CounterAction; -/// -/// /// Reducer Function -/// fn reduce(self: Rc, action: Self::Action) -> Rc { -/// let next_ctr = match action { -/// CounterAction::Double => self.counter * 2, -/// CounterAction::Square => self.counter.pow(2) -/// }; -/// -/// Self { counter: next_ctr }.into() -/// } -/// } -/// -/// #[function_component(UseReducer)] -/// fn reducer() -> Html { -/// // The use_reducer hook takes an initialization function which will be called only once. -/// let counter = use_reducer(CounterState::default); -/// -/// let double_onclick = { -/// let counter = counter.clone(); -/// Callback::from(move |_| counter.dispatch(CounterAction::Double)) -/// }; -/// let square_onclick = { -/// let counter = counter.clone(); -/// Callback::from(move |_| counter.dispatch(CounterAction::Square)) -/// }; -/// -/// html! { -/// <> -///
{ counter.counter }
-/// -/// -/// -/// -/// } -/// } -/// ``` -#[hook] -pub fn use_reducer_next(initial_fn: F) -> UseReducerHandle -where - T: Reducible + 'static, - F: FnOnce() -> T, -{ - use_reducer_base_next(initial_fn, |_, _| true) -} - -/// [`use_reducer`] but only re-renders when `prev_state != next_state`. -/// -/// This requires the state to implement [`PartialEq`] in addition to the [`Reducible`] trait -/// required by [`use_reducer`]. -#[hook] -pub fn use_reducer_eq_next(initial_fn: F) -> UseReducerHandle -where - T: Reducible + PartialEq + 'static, - F: FnOnce() -> T, -{ - use_reducer_base_next(initial_fn, T::ne) -} diff --git a/packages/yew/src/functional/hooks/use_ref.rs b/packages/yew/src/functional/hooks/use_ref.rs index aa375317b50..0bfbb9d6d9c 100644 --- a/packages/yew/src/functional/hooks/use_ref.rs +++ b/packages/yew/src/functional/hooks/use_ref.rs @@ -1,60 +1,8 @@ -use crate::functional::{hook, use_hook, use_hook_next}; -use crate::NodeRef; -use std::{cell::RefCell, rc::Rc}; +use std::cell::RefCell; +use std::rc::Rc; -/// This hook is used for obtaining a mutable reference to a stateful value. -/// Its state persists across renders. -/// -/// It is important to note that you do not get notified of state changes. -/// If you need the component to be re-rendered on state change, consider using [`use_state`](super::use_state()). -/// -/// # Example -/// ```rust -/// # use yew::prelude::*; -/// # use web_sys::HtmlInputElement; -/// # use std::rc::Rc; -/// # use std::cell::RefCell; -/// # use std::ops::{Deref, DerefMut}; -/// # -/// #[function_component(UseRef)] -/// fn ref_hook() -> Html { -/// let message = use_state(|| "".to_string()); -/// let message_count = use_mut_ref(|| 0); -/// -/// let onclick = Callback::from(move |e| { -/// let window = gloo_utils::window(); -/// -/// if *message_count.borrow_mut() > 3 { -/// window.alert_with_message("Message limit reached"); -/// } else { -/// *message_count.borrow_mut() += 1; -/// window.alert_with_message("Message sent"); -/// } -/// }); -/// -/// let onchange = { -/// let message = message.clone(); -/// Callback::from(move |e: Event| { -/// let input: HtmlInputElement = e.target_unchecked_into(); -/// message.set(input.value()) -/// }) -/// }; -/// -/// html! { -///
-/// -/// -///
-/// } -/// } -/// ``` -pub fn use_mut_ref(initial_value: impl FnOnce() -> T) -> Rc> { - use_hook( - || Rc::new(RefCell::new(initial_value())), - |state, _| state.clone(), - |_| {}, - ) -} +use crate::functional::{hook, use_hook}; +use crate::NodeRef; /// This hook is used for obtaining a mutable reference to a stateful value. /// Its state persists across renders. @@ -103,35 +51,22 @@ pub fn use_mut_ref(initial_value: impl FnOnce() -> T) -> Rc(initial_value: impl 'hook + FnOnce() -> T) -> Rc> { - use_hook_next( +pub fn use_mut_ref(initial_value: impl 'hook + FnOnce() -> T) -> Rc> { + use_hook( || Rc::new(RefCell::new(initial_value())), |state, _| state.clone(), |_| {}, ) } -/// This hook is used for obtaining a immutable reference to a stateful value. -/// Its state persists across renders. -/// -/// If you need a mutable reference, consider using [`use_mut_ref`](super::use_mut_ref). -/// If you need the component to be re-rendered on state change, consider using [`use_state`](super::use_state()). -pub fn use_ref(initial_value: impl FnOnce() -> T) -> Rc { - use_hook( - || Rc::new(initial_value()), - |state, _| Rc::clone(state), - |_| {}, - ) -} - /// This hook is used for obtaining a immutable reference to a stateful value. /// Its state persists across renders. /// /// If you need a mutable reference, consider using [`use_mut_ref`](super::use_mut_ref). /// If you need the component to be re-rendered on state change, consider using [`use_state`](super::use_state()). #[hook] -pub fn use_ref_next(initial_value: impl 'hook + FnOnce() -> T) -> Rc { - use_hook_next( +pub fn use_ref(initial_value: impl 'hook + FnOnce() -> T) -> Rc { + use_hook( || Rc::new(initial_value()), |state, _| Rc::clone(state), |_| {}, @@ -195,68 +130,7 @@ pub fn use_ref_next(initial_value: impl 'hook + FnOnce() -> T) -> Rc /// } /// /// ``` +#[hook] pub fn use_node_ref() -> NodeRef { use_hook(NodeRef::default, |state, _| state.clone(), |_| {}) } - -/// This hook is used for obtaining a [`NodeRef`]. -/// It persists across renders. -/// -/// It is important to note that you do not get notified of state changes. -/// -/// # Example -/// ```rust -/// # use wasm_bindgen::{prelude::Closure, JsCast}; -/// # use yew::{ -/// # function_component, html, use_effect_with_deps, use_node_ref, -/// # Html, -/// # }; -/// # use web_sys::{Event, HtmlElement}; -/// -/// #[function_component(UseNodeRef)] -/// pub fn node_ref_hook() -> Html { -/// let div_ref = use_node_ref(); -/// -/// { -/// let div_ref = div_ref.clone(); -/// -/// use_effect_with_deps( -/// |div_ref| { -/// let div = div_ref -/// .cast::() -/// .expect("div_ref not attached to div element"); -/// -/// let listener = Closure::::wrap(Box::new(|_| { -/// web_sys::console::log_1(&"Clicked!".into()); -/// })); -/// -/// div.add_event_listener_with_callback( -/// "click", -/// listener.as_ref().unchecked_ref(), -/// ) -/// .unwrap(); -/// -/// move || { -/// div.remove_event_listener_with_callback( -/// "click", -/// listener.as_ref().unchecked_ref(), -/// ) -/// .unwrap(); -/// } -/// }, -/// div_ref, -/// ); -/// } -/// -/// html! { -///
-/// { "Click me and watch the console log!" } -///
-/// } -/// } -/// -/// ``` -#[hook] -pub fn use_node_ref_next() -> NodeRef { - use_hook_next(NodeRef::default, |state, _| state.clone(), |_| {}) -} diff --git a/packages/yew/src/functional/hooks/use_state.rs b/packages/yew/src/functional/hooks/use_state.rs index ea79a659aa1..5ce1d264138 100644 --- a/packages/yew/src/functional/hooks/use_state.rs +++ b/packages/yew/src/functional/hooks/use_state.rs @@ -2,10 +2,7 @@ use std::fmt; use std::ops::Deref; use std::rc::Rc; -use super::{ - use_reducer, use_reducer_eq, use_reducer_eq_next, use_reducer_next, Reducible, - UseReducerDispatcher, UseReducerHandle, -}; +use super::{use_reducer, use_reducer_eq, Reducible, UseReducerDispatcher, UseReducerHandle}; use crate::functional::hook; struct UseStateReducer { @@ -57,6 +54,7 @@ where /// } /// } /// ``` +#[hook] pub fn use_state(init_fn: F) -> UseStateHandle where T: 'static, @@ -69,49 +67,10 @@ where UseStateHandle { inner: handle } } -/// This hook is used to manage state in a function component. -/// -/// # Example -/// ```rust -/// # use yew::prelude::*; -/// # use std::rc::Rc; -/// # -/// #[function_component(UseState)] -/// fn state() -> Html { -/// let counter = use_state(|| 0); -/// let onclick = { -/// let counter = counter.clone(); -/// Callback::from(move |_| counter.set(*counter + 1)) -/// }; -/// -/// -/// html! { -///
-/// -///

-/// { "Current value: " } -/// { *counter } -///

-///
-/// } -/// } -/// ``` -#[hook] -pub fn use_state_next(init_fn: F) -> UseStateHandle -where - T: 'static, - F: FnOnce() -> T, -{ - let handle = use_reducer_next(move || UseStateReducer { - value: Rc::new(init_fn()), - }); - - UseStateHandle { inner: handle } -} - /// [`use_state`] but only re-renders when `prev_state != next_state`. /// /// This hook requires the state to implement [`PartialEq`]. +#[hook] pub fn use_state_eq(init_fn: F) -> UseStateHandle where T: PartialEq + 'static, @@ -124,22 +83,6 @@ where UseStateHandle { inner: handle } } -/// [`use_state`] but only re-renders when `prev_state != next_state`. -/// -/// This hook requires the state to implement [`PartialEq`]. -#[hook] -pub fn use_state_eq_next(init_fn: F) -> UseStateHandle -where - T: PartialEq + 'static, - F: FnOnce() -> T, -{ - let handle = use_reducer_eq_next(move || UseStateReducer { - value: Rc::new(init_fn()), - }); - - UseStateHandle { inner: handle } -} - /// State handle for the [`use_state`] hook. pub struct UseStateHandle { inner: UseReducerHandle>, diff --git a/packages/yew/src/functional/mod.rs b/packages/yew/src/functional/mod.rs index 26bb5ec680c..3dd52e66402 100644 --- a/packages/yew/src/functional/mod.rs +++ b/packages/yew/src/functional/mod.rs @@ -15,7 +15,6 @@ use crate::html::{AnyScope, BaseComponent, HtmlResult}; use crate::Properties; -use scoped_tls_hkt::scoped_thread_local; use std::cell::RefCell; use std::fmt; use std::ops::DerefMut; @@ -59,13 +58,11 @@ pub use yew_macro::function_component; /// This attribute creates a hook from a normal Rust function. pub use yew_macro::hook; -scoped_thread_local!(static mut CURRENT_HOOK: HookStates); - type Msg = Box bool>; type ProcessMessage = Rc; /// A hook state. -pub struct HookStates { +pub struct HookContext { counter: usize, scope: AnyScope, process_message: ProcessMessage, @@ -73,7 +70,7 @@ pub struct HookStates { destroy_listeners: Vec>, } -impl HookStates { +impl HookContext { pub(crate) fn next_state( &mut self, initializer: INIT, @@ -114,9 +111,9 @@ impl HookStates { } } -impl fmt::Debug for HookStates { +impl fmt::Debug for HookContext { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - f.write_str("HookStates<_>") + f.write_str("HookContext<_>") } } @@ -128,13 +125,13 @@ pub trait FunctionProvider { /// Render the component. This function returns the [`Html`](crate::Html) to be rendered for the component. /// /// Equivalent of [`Component::view`](crate::html::Component::view). - fn run(props: &Self::TProps) -> HtmlResult; + fn run(ctx: &mut HookContext, props: &Self::TProps) -> HtmlResult; } /// Wrapper that allows a struct implementing [`FunctionProvider`] to be consumed as a component. pub struct FunctionComponent { _never: std::marker::PhantomData, - hook_state: RefCell, + hook_ctx: RefCell, message_queue: MsgQueue, } @@ -144,17 +141,6 @@ impl fmt::Debug for FunctionComponent { } } -impl FunctionComponent -where - T: FunctionProvider, -{ - fn with_hook_state(&self, f: impl FnOnce() -> R) -> R { - let mut hook_state = self.hook_state.borrow_mut(); - hook_state.counter = 0; - CURRENT_HOOK.set(&mut *hook_state, f) - } -} - impl BaseComponent for FunctionComponent where T: FunctionProvider + 'static, @@ -169,7 +155,7 @@ where Self { _never: std::marker::PhantomData::default(), message_queue: message_queue.clone(), - hook_state: RefCell::new(HookStates { + hook_ctx: RefCell::new(HookContext { counter: 0, scope, process_message: { @@ -197,7 +183,9 @@ where } fn view(&self, ctx: &Context) -> HtmlResult { - self.with_hook_state(|| T::run(&*ctx.props())) + let props = ctx.props(); + let mut ctx = self.hook_ctx.borrow_mut(); + T::run(&mut *ctx, props) } fn rendered(&mut self, ctx: &Context, _first_render: bool) { @@ -207,21 +195,13 @@ where } fn destroy(&mut self, _ctx: &Context) { - let mut hook_state = self.hook_state.borrow_mut(); - for hook in hook_state.destroy_listeners.drain(..) { + let mut hook_ctx = self.hook_ctx.borrow_mut(); + for hook in hook_ctx.destroy_listeners.drain(..) { hook() } } } -pub(crate) fn get_current_scope() -> Option { - if CURRENT_HOOK.is_set() { - Some(CURRENT_HOOK.with(|state| state.scope.clone())) - } else { - None - } -} - impl SealedBaseComponent for FunctionComponent where T: FunctionProvider + 'static {} #[derive(Clone, Default)] diff --git a/packages/yew/tests/mod.rs b/packages/yew/tests/mod.rs index a8a7adbd897..6ea61c78267 100644 --- a/packages/yew/tests/mod.rs +++ b/packages/yew/tests/mod.rs @@ -2,31 +2,27 @@ mod common; use common::obtain_result; use wasm_bindgen_test::*; -use yew::functional::{FunctionComponent, FunctionProvider}; -use yew::{html, HtmlResult, Properties}; +use yew::prelude::*; wasm_bindgen_test::wasm_bindgen_test_configure!(run_in_browser); #[wasm_bindgen_test] fn props_are_passed() { - struct PropsPassedFunction {} #[derive(Properties, Clone, PartialEq)] struct PropsPassedFunctionProps { value: String, } - impl FunctionProvider for PropsPassedFunction { - type TProps = PropsPassedFunctionProps; - fn run(props: &Self::TProps) -> HtmlResult { - assert_eq!(&props.value, "props"); - return Ok(html! { -
- {"done"} -
- }); + #[function_component] + fn PropsComponent(props: &PropsPassedFunctionProps) -> Html { + assert_eq!(&props.value, "props"); + html! { +
+ {"done"} +
} } - type PropsComponent = FunctionComponent; + yew::start_app_with_props_in_element::( gloo_utils::document().get_element_by_id("output").unwrap(), PropsPassedFunctionProps { diff --git a/packages/yew/tests/suspense.rs b/packages/yew/tests/suspense.rs index 302714d2621..667458bc475 100644 --- a/packages/yew/tests/suspense.rs +++ b/packages/yew/tests/suspense.rs @@ -44,6 +44,7 @@ async fn suspense_works() { } } + #[hook] pub fn use_sleep() -> SuspensionResult> { let sleep_state = use_reducer(SleepState::new); @@ -182,6 +183,7 @@ async fn suspense_not_suspended_at_start() { } } + #[hook] pub fn use_sleep() -> SuspensionResult> { let sleep_state = use_reducer(SleepState::new); @@ -297,6 +299,7 @@ async fn suspense_nested_suspense_works() { } } + #[hook] pub fn use_sleep() -> SuspensionResult> { let sleep_state = use_reducer(SleepState::new); @@ -430,6 +433,7 @@ async fn effects_not_run_when_suspended() { } } + #[hook] pub fn use_sleep() -> SuspensionResult> { let sleep_state = use_reducer(SleepState::new); diff --git a/packages/yew/tests/use_context.rs b/packages/yew/tests/use_context.rs index f9b942f54a8..e7c46aa3f8d 100644 --- a/packages/yew/tests/use_context.rs +++ b/packages/yew/tests/use_context.rs @@ -14,10 +14,12 @@ fn use_context_scoping_works() { #[function_component] fn ExpectNoContextComponent() -> Html { + let example_context = use_context::().unwrap(); + if use_context::().is_some() { console_log!( "Context should be None here, but was {:?}!", - use_context::().unwrap() + example_context ); }; html! { @@ -73,32 +75,44 @@ fn use_context_works_with_multiple_types() { #[function_component] fn Test1() -> Html { - assert_eq!(use_context::(), Some(ContextA(2))); - assert_eq!(use_context::(), Some(ContextB(1))); + let ctx_a = use_context::(); + let ctx_b = use_context::(); + + assert_eq!(ctx_a, Some(ContextA(2))); + assert_eq!(ctx_b, Some(ContextB(1))); html! {} } #[function_component] fn Test2() -> Html { - assert_eq!(use_context::(), Some(ContextA(0))); - assert_eq!(use_context::(), Some(ContextB(1))); + let ctx_a = use_context::(); + let ctx_b = use_context::(); + + assert_eq!(ctx_a, Some(ContextA(0))); + assert_eq!(ctx_b, Some(ContextB(1))); html! {} } #[function_component] fn Test3() -> Html { - assert_eq!(use_context::(), Some(ContextA(0))); - assert_eq!(use_context::(), None); + let ctx_a = use_context::(); + let ctx_b = use_context::(); + + assert_eq!(ctx_a, Some(ContextA(0))); + assert_eq!(ctx_b, None); html! {} } #[function_component] fn Test4() -> Html { - assert_eq!(use_context::(), None); - assert_eq!(use_context::(), None); + let ctx_a = use_context::(); + let ctx_b = use_context::(); + + assert_eq!(ctx_a, None); + assert_eq!(ctx_b, None); html! {} } From 72518e2f984ae4ee913cba1b947f3f52a36ee7a2 Mon Sep 17 00:00:00 2001 From: Kaede Hoshikawa Date: Sat, 22 Jan 2022 19:04:38 +0900 Subject: [PATCH 12/42] Fix HookContext, add tests on non-possible locations. --- packages/yew-macro/src/lib.rs | 1 + .../generic-props-fail.stderr | 52 +++++++++---------- .../hook_location-fail.rs | 35 +++++++++++++ .../hook_location-fail.rs.stderr | 29 +++++++++++ .../hook_location-pass.rs | 26 ++++++++++ packages/yew/src/functional/mod.rs | 1 + packages/yew/src/virtual_dom/vsuspense.rs | 1 + packages/yew/tests/use_context.rs | 4 +- .../function-components/custom-hooks.mdx | 5 +- .../function-components/introduction.mdx | 5 ++ website/docs/concepts/suspense.mdx | 2 + 11 files changed, 132 insertions(+), 29 deletions(-) create mode 100644 packages/yew-macro/tests/function_component_attr/hook_location-fail.rs create mode 100644 packages/yew-macro/tests/function_component_attr/hook_location-fail.rs.stderr create mode 100644 packages/yew-macro/tests/function_component_attr/hook_location-pass.rs diff --git a/packages/yew-macro/src/lib.rs b/packages/yew-macro/src/lib.rs index 9efe3071c5e..b01a9595c44 100644 --- a/packages/yew-macro/src/lib.rs +++ b/packages/yew-macro/src/lib.rs @@ -124,6 +124,7 @@ pub fn classes(input: TokenStream) -> TokenStream { TokenStream::from(classes.into_token_stream()) } +#[proc_macro_error::proc_macro_error] #[proc_macro_attribute] pub fn function_component(attr: TokenStream, item: TokenStream) -> proc_macro::TokenStream { let item = parse_macro_input!(item as FunctionComponent); diff --git a/packages/yew-macro/tests/function_component_attr/generic-props-fail.stderr b/packages/yew-macro/tests/function_component_attr/generic-props-fail.stderr index 9c354326629..aea648dd1f2 100644 --- a/packages/yew-macro/tests/function_component_attr/generic-props-fail.stderr +++ b/packages/yew-macro/tests/function_component_attr/generic-props-fail.stderr @@ -29,36 +29,36 @@ error[E0277]: the trait bound `FunctionComponent as BaseComponent> error[E0599]: the function or associated item `new` exists for struct `VChild>>`, but its trait bounds were not satisfied - --> tests/function_component_attr/generic-props-fail.rs:27:14 - | -27 | html! { /> }; - | ^^^^ function or associated item cannot be called on `VChild>>` due to unsatisfied trait bounds - | - ::: $WORKSPACE/packages/yew/src/functional/mod.rs - | - | pub struct FunctionComponent { - | ----------------------------------------------------------- doesn't satisfy `_: BaseComponent` - | - = note: the following trait bounds were not satisfied: - `FunctionComponent>: BaseComponent` + --> tests/function_component_attr/generic-props-fail.rs:27:14 + | +27 | html! { /> }; + | ^^^^ function or associated item cannot be called on `VChild>>` due to unsatisfied trait bounds + | + ::: $WORKSPACE/packages/yew/src/functional/mod.rs + | + | pub struct FunctionComponent { + | ----------------------------------------------------------- doesn't satisfy `_: BaseComponent` + | + = note: the following trait bounds were not satisfied: + `FunctionComponent>: BaseComponent` error[E0277]: the trait bound `MissingTypeBounds: yew::Properties` is not satisfied - --> tests/function_component_attr/generic-props-fail.rs:27:14 - | -27 | html! { /> }; - | ^^^^ the trait `yew::Properties` is not implemented for `MissingTypeBounds` - | + --> tests/function_component_attr/generic-props-fail.rs:27:14 + | +27 | html! { /> }; + | ^^^^ the trait `yew::Properties` is not implemented for `MissingTypeBounds` + | note: required because of the requirements on the impl of `FunctionProvider` for `CompFunctionProvider` - --> tests/function_component_attr/generic-props-fail.rs:8:1 - | -8 | #[function_component(Comp)] - | ^^^^^^^^^^^^^^^^^^^^^^^^^^^ + --> tests/function_component_attr/generic-props-fail.rs:8:1 + | +8 | #[function_component(Comp)] + | ^^^^^^^^^^^^^^^^^^^^^^^^^^^ note: required by a bound in `FunctionComponent` - --> $WORKSPACE/packages/yew/src/functional/mod.rs - | - | pub struct FunctionComponent { - | ^^^^^^^^^^^^^^^^ required by this bound in `FunctionComponent` - = note: this error originates in the attribute macro `function_component` (in Nightly builds, run with -Z macro-backtrace for more info) + --> $WORKSPACE/packages/yew/src/functional/mod.rs + | + | pub struct FunctionComponent { + | ^^^^^^^^^^^^^^^^ required by this bound in `FunctionComponent` + = note: this error originates in the attribute macro `function_component` (in Nightly builds, run with -Z macro-backtrace for more info) error[E0107]: missing generics for type alias `Comp` --> tests/function_component_attr/generic-props-fail.rs:30:14 diff --git a/packages/yew-macro/tests/function_component_attr/hook_location-fail.rs b/packages/yew-macro/tests/function_component_attr/hook_location-fail.rs new file mode 100644 index 00000000000..8eed83ec507 --- /dev/null +++ b/packages/yew-macro/tests/function_component_attr/hook_location-fail.rs @@ -0,0 +1,35 @@ +use yew::prelude::*; + +#[derive(Debug, PartialEq, Clone)] +struct Ctx; + +#[function_component] +fn Comp() -> Html { + if let Some(_m) = use_context::() { + use_context::().unwrap(); + todo!() + } + + let _ = || { + use_context::().unwrap(); + todo!() + }; + + for _ in 0..10 { + use_context::().unwrap(); + } + + match use_context::() { + Some(_) => use_context::(), + None => { + todo!() + } + } + + loop { + use_context::().unwrap(); + todo!() + } +} + +fn main() {} diff --git a/packages/yew-macro/tests/function_component_attr/hook_location-fail.rs.stderr b/packages/yew-macro/tests/function_component_attr/hook_location-fail.rs.stderr new file mode 100644 index 00000000000..7432f877747 --- /dev/null +++ b/packages/yew-macro/tests/function_component_attr/hook_location-fail.rs.stderr @@ -0,0 +1,29 @@ +error: hooks cannot be called at this position. + --> tests/function_component_attr/hook_location-fail.rs:9:9 + | +9 | use_context::().unwrap(); + | ^^^^^^^^^^^ + +error: hooks cannot be called at this position. + --> tests/function_component_attr/hook_location-fail.rs:14:9 + | +14 | use_context::().unwrap(); + | ^^^^^^^^^^^ + +error: hooks cannot be called at this position. + --> tests/function_component_attr/hook_location-fail.rs:19:9 + | +19 | use_context::().unwrap(); + | ^^^^^^^^^^^ + +error: hooks cannot be called at this position. + --> tests/function_component_attr/hook_location-fail.rs:23:20 + | +23 | Some(_) => use_context::(), + | ^^^^^^^^^^^ + +error: hooks cannot be called at this position. + --> tests/function_component_attr/hook_location-fail.rs:30:9 + | +30 | use_context::().unwrap(); + | ^^^^^^^^^^^ diff --git a/packages/yew-macro/tests/function_component_attr/hook_location-pass.rs b/packages/yew-macro/tests/function_component_attr/hook_location-pass.rs new file mode 100644 index 00000000000..3e95f08c546 --- /dev/null +++ b/packages/yew-macro/tests/function_component_attr/hook_location-pass.rs @@ -0,0 +1,26 @@ +use yew::prelude::*; + +#[derive(Debug, PartialEq, Clone)] +struct Ctx; + +#[function_component] +fn Comp() -> Html { + use_context::().unwrap(); + + if let Some(_m) = use_context::() { + todo!() + } + + let _ctx = { use_context::() }; + + match use_context::() { + Some(_) => { + todo!() + } + None => { + todo!() + } + } +} + +fn main() {} diff --git a/packages/yew/src/functional/mod.rs b/packages/yew/src/functional/mod.rs index 3dd52e66402..f0285b06fd7 100644 --- a/packages/yew/src/functional/mod.rs +++ b/packages/yew/src/functional/mod.rs @@ -185,6 +185,7 @@ where fn view(&self, ctx: &Context) -> HtmlResult { let props = ctx.props(); let mut ctx = self.hook_ctx.borrow_mut(); + ctx.counter = 0; T::run(&mut *ctx, props) } diff --git a/packages/yew/src/virtual_dom/vsuspense.rs b/packages/yew/src/virtual_dom/vsuspense.rs index e2a73bb1446..37469389d17 100644 --- a/packages/yew/src/virtual_dom/vsuspense.rs +++ b/packages/yew/src/virtual_dom/vsuspense.rs @@ -206,6 +206,7 @@ mod ssr_tests { } } + #[hook] pub fn use_sleep() -> SuspensionResult> { let sleep_state = use_reducer(SleepState::new); diff --git a/packages/yew/tests/use_context.rs b/packages/yew/tests/use_context.rs index e7c46aa3f8d..0f655c39692 100644 --- a/packages/yew/tests/use_context.rs +++ b/packages/yew/tests/use_context.rs @@ -14,9 +14,9 @@ fn use_context_scoping_works() { #[function_component] fn ExpectNoContextComponent() -> Html { - let example_context = use_context::().unwrap(); + let example_context = use_context::(); - if use_context::().is_some() { + if example_context.is_some() { console_log!( "Context should be None here, but was {:?}!", example_context diff --git a/website/docs/concepts/function-components/custom-hooks.mdx b/website/docs/concepts/function-components/custom-hooks.mdx index 7dbd3e7b217..fd1a39cb79a 100644 --- a/website/docs/concepts/function-components/custom-hooks.mdx +++ b/website/docs/concepts/function-components/custom-hooks.mdx @@ -39,13 +39,15 @@ If we build another component which keeps track of the an event, instead of copying the code we can move the logic into a custom hook. We'll start by creating a new function called `use_event`. -The `use_` prefix conventionally denotes that a function is a hook. +The `use_` prefix denotes that a function is a hook. This function will take an event target, a event type and a callback. +All hooks must be marked by `#[hook]` to function as as hook. ```rust use web_sys::{Event, EventTarget}; use std::borrow::Cow; use gloo::events::EventListener; +#[hook] pub fn use_event(target: &EventTarget, event_type: E, callback: F) where E: Into>, @@ -65,6 +67,7 @@ use std::borrow::Cow; use std::rc::Rc; use gloo::events::EventListener; +#[hook] pub fn use_event(target: &EventTarget, event_type: E, callback: F) where E: Into>, diff --git a/website/docs/concepts/function-components/introduction.mdx b/website/docs/concepts/function-components/introduction.mdx index 7aab0326701..c75e0ae0213 100644 --- a/website/docs/concepts/function-components/introduction.mdx +++ b/website/docs/concepts/function-components/introduction.mdx @@ -40,6 +40,11 @@ The `#[function_component]` attribute is a procedural macro which automatically Hooks are functions that let you "hook into" components' state and/or lifecycle and perform actions. Yew comes with a few pre-defined Hooks. You can also create your own. +Hooks can only be used at the following locations: +- Top level of a function / hook. +- If condition of a function / hook given it's not branched. +- Blocks inside a function / hook given it's not branched. + #### Pre-defined Hooks Yew comes with the following predefined Hooks: diff --git a/website/docs/concepts/suspense.mdx b/website/docs/concepts/suspense.mdx index ad7e041be39..444ccf4be22 100644 --- a/website/docs/concepts/suspense.mdx +++ b/website/docs/concepts/suspense.mdx @@ -60,6 +60,7 @@ struct User { name: String, } +#[hook] fn use_user() -> SuspensionResult { match load_user() { // If a user is loaded, then we return it as Ok(user). @@ -96,6 +97,7 @@ fn on_load_user_complete(_fn: F) { todo!() // implementation omitted. } +#[hook] fn use_user() -> SuspensionResult { match load_user() { // If a user is loaded, then we return it as Ok(user). From 38e06b1b693241e267bac1b076a8a842f178bf63 Mon Sep 17 00:00:00 2001 From: Kaede Hoshikawa Date: Sat, 22 Jan 2022 19:08:00 +0900 Subject: [PATCH 13/42] Fix stderr for trybuild. --- .../{hook_location-fail.rs.stderr => hook_location-fail.stderr} | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename packages/yew-macro/tests/function_component_attr/{hook_location-fail.rs.stderr => hook_location-fail.stderr} (100%) diff --git a/packages/yew-macro/tests/function_component_attr/hook_location-fail.rs.stderr b/packages/yew-macro/tests/function_component_attr/hook_location-fail.stderr similarity index 100% rename from packages/yew-macro/tests/function_component_attr/hook_location-fail.rs.stderr rename to packages/yew-macro/tests/function_component_attr/hook_location-fail.stderr From 19af0b3cd4aa148a069562b04b0d67d387089e0b Mon Sep 17 00:00:00 2001 From: Kaede Hoshikawa Date: Sat, 22 Jan 2022 19:10:34 +0900 Subject: [PATCH 14/42] Add 1 more test case. --- .../hook_location-fail.rs | 4 ++++ .../hook_location-fail.stderr | 20 +++++++++++++++---- 2 files changed, 20 insertions(+), 4 deletions(-) diff --git a/packages/yew-macro/tests/function_component_attr/hook_location-fail.rs b/packages/yew-macro/tests/function_component_attr/hook_location-fail.rs index 8eed83ec507..37637ef7cac 100644 --- a/packages/yew-macro/tests/function_component_attr/hook_location-fail.rs +++ b/packages/yew-macro/tests/function_component_attr/hook_location-fail.rs @@ -19,6 +19,10 @@ fn Comp() -> Html { use_context::().unwrap(); } + while let Some(_m) = use_context::() { + use_context::().unwrap(); + } + match use_context::() { Some(_) => use_context::(), None => { diff --git a/packages/yew-macro/tests/function_component_attr/hook_location-fail.stderr b/packages/yew-macro/tests/function_component_attr/hook_location-fail.stderr index 7432f877747..2559f7db859 100644 --- a/packages/yew-macro/tests/function_component_attr/hook_location-fail.stderr +++ b/packages/yew-macro/tests/function_component_attr/hook_location-fail.stderr @@ -17,13 +17,25 @@ error: hooks cannot be called at this position. | ^^^^^^^^^^^ error: hooks cannot be called at this position. - --> tests/function_component_attr/hook_location-fail.rs:23:20 + --> tests/function_component_attr/hook_location-fail.rs:22:26 | -23 | Some(_) => use_context::(), +22 | while let Some(_m) = use_context::() { + | ^^^^^^^^^^^ + +error: hooks cannot be called at this position. + --> tests/function_component_attr/hook_location-fail.rs:23:9 + | +23 | use_context::().unwrap(); + | ^^^^^^^^^^^ + +error: hooks cannot be called at this position. + --> tests/function_component_attr/hook_location-fail.rs:27:20 + | +27 | Some(_) => use_context::(), | ^^^^^^^^^^^ error: hooks cannot be called at this position. - --> tests/function_component_attr/hook_location-fail.rs:30:9 + --> tests/function_component_attr/hook_location-fail.rs:34:9 | -30 | use_context::().unwrap(); +34 | use_context::().unwrap(); | ^^^^^^^^^^^ From 49454cd5d927b8ff5938cdce42158238c6d32f7b Mon Sep 17 00:00:00 2001 From: Kaede Hoshikawa Date: Sat, 22 Jan 2022 19:20:37 +0900 Subject: [PATCH 15/42] Adjust doc location. --- packages/yew-macro/src/hook/mod.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/yew-macro/src/hook/mod.rs b/packages/yew-macro/src/hook/mod.rs index 6a038bb6493..77522c6defa 100644 --- a/packages/yew-macro/src/hook/mod.rs +++ b/packages/yew-macro/src/hook/mod.rs @@ -113,8 +113,8 @@ When used in function components and hooks, this hook is equivalent to: let boxed_fn_type = quote! { ::std::boxed::Box }; let output = quote! { - #[doc = #doc_text] #(#attrs)* + #[doc = #doc_text] #vis #fn_token #ident #generics (#inputs) #hook_return_type #where_clause { // fn #inner_fn_ident #generics (#ctx_ident: &mut ::yew::functional::HookContext, #inputs) -> #output_type #block From eb85f472bd54c36191899325e9d68e67da30002b Mon Sep 17 00:00:00 2001 From: Kaede Hoshikawa Date: Sat, 22 Jan 2022 19:53:21 +0900 Subject: [PATCH 16/42] Pretty print hook signature. --- packages/yew-macro/Cargo.toml | 1 + packages/yew-macro/src/hook/mod.rs | 34 +++++++++++++++--------- packages/yew-macro/src/hook/signature.rs | 17 +++++------- packages/yew/src/functional/mod.rs | 5 ++-- 4 files changed, 31 insertions(+), 26 deletions(-) diff --git a/packages/yew-macro/Cargo.toml b/packages/yew-macro/Cargo.toml index 2f660064145..85fb249eff4 100644 --- a/packages/yew-macro/Cargo.toml +++ b/packages/yew-macro/Cargo.toml @@ -23,6 +23,7 @@ proc-macro2 = "1" quote = "1" syn = { version = "1", features = ["full", "extra-traits", "visit-mut"] } once_cell = "1" +prettyplease = "0.1.1" # testing [dev-dependencies] diff --git a/packages/yew-macro/src/hook/mod.rs b/packages/yew-macro/src/hook/mod.rs index 77522c6defa..8f5c8b956f3 100644 --- a/packages/yew-macro/src/hook/mod.rs +++ b/packages/yew-macro/src/hook/mod.rs @@ -1,10 +1,9 @@ use proc_macro2::{Span, TokenStream}; use proc_macro_error::emit_error; use quote::quote; -use quote::ToTokens; use syn::parse::{Parse, ParseStream}; use syn::visit_mut; -use syn::{Ident, ItemFn, LitStr, ReturnType, Signature}; +use syn::{parse_file, Ident, ItemFn, LitStr, ReturnType, Signature}; mod body; mod lifetime; @@ -51,6 +50,21 @@ impl Parse for HookFn { pub fn hook_impl(component: HookFn) -> syn::Result { let HookFn { inner } = component; + let ItemFn { + vis, + sig, + mut block, + attrs, + } = inner; + + let sig_s = quote! { #vis #sig { + __yew_macro_dummy_function_body__ + } } + .to_string(); + + let sig_file = parse_file(&sig_s).unwrap(); + let sig_formatted = prettyplease::unparse(&sig_file); + let doc_text = LitStr::new( &format!( r#" @@ -62,18 +76,14 @@ When used in function components and hooks, this hook is equivalent to: {} ``` "#, - inner.sig.to_token_stream() + sig_formatted.replace( + "__yew_macro_dummy_function_body__", + "/* implementation omitted */" + ) ), Span::mixed_site(), ); - let ItemFn { - vis, - sig, - mut block, - attrs, - } = inner; - let hook_sig = HookSignature::rewrite(&sig); let Signature { @@ -85,7 +95,7 @@ When used in function components and hooks, this hook is equivalent to: .. } = hook_sig.sig; - let hook_lifetime = hook_sig.hook_lifetime.as_ref(); + let hook_lifetime = &hook_sig.hook_lifetime; let output_type = &hook_sig.output_type; let hook_struct_name = Ident::new("HookProvider", Span::mixed_site()); @@ -100,7 +110,7 @@ When used in function components and hooks, this hook is equivalent to: let mut body_rewriter = BodyRewriter::default(); visit_mut::visit_block_mut(&mut body_rewriter, &mut *block); - let hook_lifetime_plus = hook_lifetime.map(|m| quote! { #m + }); + let hook_lifetime_plus = quote! { #hook_lifetime + }; let inner_ident = Ident::new("inner", Span::mixed_site()); // let inner_fn_ident = Ident::new("inner_fn", Span::mixed_site()); diff --git a/packages/yew-macro/src/hook/signature.rs b/packages/yew-macro/src/hook/signature.rs index 30a07777d1a..737d153a51c 100644 --- a/packages/yew-macro/src/hook/signature.rs +++ b/packages/yew-macro/src/hook/signature.rs @@ -10,17 +10,14 @@ use syn::{ use super::lifetime; pub struct HookSignature { - pub hook_lifetime: Option, + pub hook_lifetime: Lifetime, pub sig: Signature, pub output_type: Type, } impl HookSignature { - fn rewrite_return_type( - hook_lifetime: Option<&Lifetime>, - rt_type: &ReturnType, - ) -> (ReturnType, Type) { - let bound = hook_lifetime.map(|m| quote! { #m + }); + fn rewrite_return_type(hook_lifetime: &Lifetime, rt_type: &ReturnType) -> (ReturnType, Type) { + let bound = quote! { #hook_lifetime + }; match rt_type { ReturnType::Default => ( @@ -54,7 +51,7 @@ impl HookSignature { .. } = sig; - let hook_lifetime = if !generics.params.is_empty() { + let hook_lifetime = { let hook_lifetime = Lifetime::new("'hook", Span::mixed_site()); generics.params = { let elided_lifetimes = &lifetimes.elided; @@ -94,12 +91,10 @@ impl HookSignature { generics.where_clause = Some(where_clause); - Some(hook_lifetime) - } else { - None + hook_lifetime }; - let (output, output_type) = Self::rewrite_return_type(hook_lifetime.as_ref(), return_type); + let (output, output_type) = Self::rewrite_return_type(&hook_lifetime, return_type); sig.output = output; Self { diff --git a/packages/yew/src/functional/mod.rs b/packages/yew/src/functional/mod.rs index f0285b06fd7..850e9298bb1 100644 --- a/packages/yew/src/functional/mod.rs +++ b/packages/yew/src/functional/mod.rs @@ -61,7 +61,7 @@ pub use yew_macro::hook; type Msg = Box bool>; type ProcessMessage = Rc; -/// A hook state. +/// A hook context to be passed to hooks. pub struct HookContext { counter: usize, scope: AnyScope, @@ -229,8 +229,7 @@ impl MsgQueue { /// for more details on how to use the hook updater to provide function components /// the necessary callbacks to update the underlying state. #[derive(Clone)] -#[allow(missing_debug_implementations)] -pub struct HookUpdater { +pub(crate) struct HookUpdater { hook: Rc>, process_message: ProcessMessage, } From 4f0260c6711d73b79aec234a7a13efd763a05bd2 Mon Sep 17 00:00:00 2001 From: Kaede Hoshikawa Date: Sat, 22 Jan 2022 20:23:02 +0900 Subject: [PATCH 17/42] Fix Items & std::ops::Fn*. --- packages/yew-macro/src/hook/body.rs | 7 ++- packages/yew-macro/src/hook/lifetime.rs | 56 +++++++++++++++++++- packages/yew/src/functional/hooks/use_ref.rs | 4 +- 3 files changed, 63 insertions(+), 4 deletions(-) diff --git a/packages/yew-macro/src/hook/body.rs b/packages/yew-macro/src/hook/body.rs index ee01930d2e6..344c2dff210 100644 --- a/packages/yew-macro/src/hook/body.rs +++ b/packages/yew-macro/src/hook/body.rs @@ -4,7 +4,7 @@ use syn::spanned::Spanned; use syn::visit_mut::VisitMut; use syn::{ parse_quote_spanned, visit_mut, Expr, ExprCall, ExprClosure, ExprForLoop, ExprIf, ExprLoop, - ExprMatch, ExprWhile, Ident, + ExprMatch, ExprWhile, Ident, Item, }; #[derive(Debug, Default)] @@ -110,4 +110,9 @@ impl VisitMut for BodyRewriter { self.with_branch(|m| visit_mut::visit_expr_mut(m, &mut i.cond)); self.with_branch(|m| visit_mut::visit_block_mut(m, &mut i.body)); } + + fn visit_item_mut(&mut self, _i: &mut Item) { + // We don't do anything for items. + // for components / hooks in other components / hooks, apply the attribute again. + } } diff --git a/packages/yew-macro/src/hook/lifetime.rs b/packages/yew-macro/src/hook/lifetime.rs index 7f108c7f157..22b6c3b289a 100644 --- a/packages/yew-macro/src/hook/lifetime.rs +++ b/packages/yew-macro/src/hook/lifetime.rs @@ -1,6 +1,9 @@ use proc_macro2::Span; use syn::visit_mut::{self, VisitMut}; -use syn::{GenericArgument, Lifetime, Receiver, TypeReference}; +use syn::{ + GenericArgument, Lifetime, ParenthesizedGenericArguments, Receiver, TypeImplTrait, + TypeParamBound, TypeReference, +}; // borrowed from the awesome async-trait crate. pub struct CollectLifetimes { @@ -8,6 +11,9 @@ pub struct CollectLifetimes { pub explicit: Vec, pub name: &'static str, pub default_span: Span, + + pub impl_trait_ctr: u64, + pub impl_fn_ctr: u64, } impl CollectLifetimes { @@ -17,9 +23,20 @@ impl CollectLifetimes { explicit: Vec::new(), name, default_span, + + impl_trait_ctr: 0, + impl_fn_ctr: 0, } } + fn is_impl_trait(&self) -> bool { + self.impl_trait_ctr > 0 + } + + fn is_impl_fn(&self) -> bool { + self.impl_fn_ctr > 0 + } + fn visit_opt_lifetime(&mut self, lifetime: &mut Option) { match lifetime { None => *lifetime = Some(self.next_lifetime(None)), @@ -52,14 +69,51 @@ impl VisitMut for CollectLifetimes { } fn visit_type_reference_mut(&mut self, ty: &mut TypeReference) { + // We don't rewrite references in the impl FnOnce(&arg) + if self.is_impl_fn() { + return; + } + self.visit_opt_lifetime(&mut ty.lifetime); visit_mut::visit_type_reference_mut(self, ty); } fn visit_generic_argument_mut(&mut self, gen: &mut GenericArgument) { + // We don't rewrite types in the impl FnOnce(&arg) -> Type<'_> + if self.is_impl_fn() { + return; + } + if let GenericArgument::Lifetime(lifetime) = gen { self.visit_lifetime(lifetime); } visit_mut::visit_generic_argument_mut(self, gen); } + + fn visit_type_impl_trait_mut(&mut self, impl_trait: &mut TypeImplTrait) { + self.impl_trait_ctr += 1; + + impl_trait + .bounds + .insert(0, TypeParamBound::Lifetime(self.next_lifetime(None))); + + visit_mut::visit_type_impl_trait_mut(self, impl_trait); + + self.impl_trait_ctr -= 1; + } + + fn visit_parenthesized_generic_arguments_mut( + &mut self, + generic_args: &mut ParenthesizedGenericArguments, + ) { + if self.is_impl_trait() { + self.impl_fn_ctr += 1; + } + + visit_mut::visit_parenthesized_generic_arguments_mut(self, generic_args); + + if self.is_impl_trait() { + self.impl_fn_ctr -= 1; + } + } } diff --git a/packages/yew/src/functional/hooks/use_ref.rs b/packages/yew/src/functional/hooks/use_ref.rs index 0bfbb9d6d9c..a6e7ad585ef 100644 --- a/packages/yew/src/functional/hooks/use_ref.rs +++ b/packages/yew/src/functional/hooks/use_ref.rs @@ -51,7 +51,7 @@ use crate::NodeRef; /// } /// ``` #[hook] -pub fn use_mut_ref(initial_value: impl 'hook + FnOnce() -> T) -> Rc> { +pub fn use_mut_ref(initial_value: impl FnOnce() -> T) -> Rc> { use_hook( || Rc::new(RefCell::new(initial_value())), |state, _| state.clone(), @@ -65,7 +65,7 @@ pub fn use_mut_ref(initial_value: impl 'hook + FnOnce() -> T) -> Rc< /// If you need a mutable reference, consider using [`use_mut_ref`](super::use_mut_ref). /// If you need the component to be re-rendered on state change, consider using [`use_state`](super::use_state()). #[hook] -pub fn use_ref(initial_value: impl 'hook + FnOnce() -> T) -> Rc { +pub fn use_ref(initial_value: impl FnOnce() -> T) -> Rc { use_hook( || Rc::new(initial_value()), |state, _| Rc::clone(state), From 344ae10e44db27b0e6d7b6270ef19c3a2b7fc8b2 Mon Sep 17 00:00:00 2001 From: Kaede Hoshikawa Date: Sat, 22 Jan 2022 21:08:35 +0900 Subject: [PATCH 18/42] Add use_memo. --- packages/yew/src/functional/hooks/mod.rs | 2 + .../yew/src/functional/hooks/use_context.rs | 53 +++++++++--------- packages/yew/src/functional/hooks/use_memo.rs | 56 +++++++++++++++++++ packages/yew/src/functional/hooks/use_ref.rs | 14 ----- packages/yew/tests/use_memo.rs | 53 ++++++++++++++++++ 5 files changed, 136 insertions(+), 42 deletions(-) create mode 100644 packages/yew/src/functional/hooks/use_memo.rs create mode 100644 packages/yew/tests/use_memo.rs diff --git a/packages/yew/src/functional/hooks/mod.rs b/packages/yew/src/functional/hooks/mod.rs index 11d1af1982d..b4805b7ceb0 100644 --- a/packages/yew/src/functional/hooks/mod.rs +++ b/packages/yew/src/functional/hooks/mod.rs @@ -1,11 +1,13 @@ mod use_context; mod use_effect; +mod use_memo; mod use_reducer; mod use_ref; mod use_state; pub use use_context::*; pub use use_effect::*; +pub use use_memo::*; pub use use_reducer::*; pub use use_ref::*; pub use use_state::*; diff --git a/packages/yew/src/functional/hooks/use_context.rs b/packages/yew/src/functional/hooks/use_context.rs index d082e1dace0..29f75597e86 100644 --- a/packages/yew/src/functional/hooks/use_context.rs +++ b/packages/yew/src/functional/hooks/use_context.rs @@ -1,5 +1,6 @@ +use crate::callback::Callback; use crate::context::ContextHandle; -use crate::functional::{hook, use_component_scope, use_hook}; +use crate::functional::{hook, use_component_scope, use_state}; /// Hook for consuming context values in function components. /// The context of the type passed as `T` is returned. If there is no such context in scope, `None` is returned. @@ -30,36 +31,32 @@ use crate::functional::{hook, use_component_scope, use_hook}; /// ``` #[hook] pub fn use_context() -> Option { - struct UseContextState { - initialized: bool, - context: Option<(T2, ContextHandle)>, + struct State { + context: Option<(T, ContextHandle)>, + } + + impl PartialEq for State + where + T: Clone + PartialEq + 'static, + { + fn eq(&self, rhs: &Self) -> bool { + self.context.as_ref().map(|m| &m.0) == rhs.context.as_ref().map(|m| &m.0) + } } let scope = use_component_scope(); - use_hook( - move || UseContextState { - initialized: false, - context: None, - }, - |state: &mut UseContextState, updater| { - if !state.initialized { - state.initialized = true; - let callback = move |ctx: T| { - updater.callback(|state: &mut UseContextState| { - if let Some(context) = &mut state.context { - context.0 = ctx; - } - true - }); - }; - state.context = scope.context::(callback.into()); - } + let val = use_state(|| -> Option { None }); + let state = { + let val_dispatcher = val.setter(); + use_state(move || State { + context: scope.context::(Callback::from(move |m| { + val_dispatcher.clone().set(Some(m)); + })), + }) + }; - Some(state.context.as_ref()?.0.clone()) - }, - |state| { - state.context = None; - }, - ) + (*val) + .clone() + .or_else(move || state.context.as_ref().map(|m| m.0.clone())) } diff --git a/packages/yew/src/functional/hooks/use_memo.rs b/packages/yew/src/functional/hooks/use_memo.rs new file mode 100644 index 00000000000..8151c1400df --- /dev/null +++ b/packages/yew/src/functional/hooks/use_memo.rs @@ -0,0 +1,56 @@ +use crate::functional::{hook, use_hook}; +use std::rc::Rc; + +/// This hook is used for obtaining a immutable reference to a value that is recalculated when it's +/// dependency changes. +#[hook] +pub fn use_memo(memo_fn: impl FnOnce(&Dependents) -> T, deps: Dependents) -> Rc +where + T: 'static, + Dependents: 'static + PartialEq, +{ + let deps = Rc::new(deps); + + pub struct UseMemo + where + T: 'static, + Dependents: 'static + PartialEq, + { + deps: Option>, + val: Option>, + } + + use_hook( + || { + let state: UseMemo = UseMemo { + val: None, + deps: None, + }; + + state + }, + move |state, _updater| match state.val.clone() { + Some(m) => { + if Some(&deps) != state.deps.as_ref() { + let val = Rc::new(memo_fn(&deps)); + + state.val = Some(val.clone()); + state.deps = Some(deps); + + val + } else { + m + } + } + None => { + let val = Rc::new(memo_fn(&deps)); + + state.val = Some(val.clone()); + state.deps = Some(deps); + + val + } + }, + |_| {}, + ) +} diff --git a/packages/yew/src/functional/hooks/use_ref.rs b/packages/yew/src/functional/hooks/use_ref.rs index a6e7ad585ef..76e1f087d46 100644 --- a/packages/yew/src/functional/hooks/use_ref.rs +++ b/packages/yew/src/functional/hooks/use_ref.rs @@ -59,20 +59,6 @@ pub fn use_mut_ref(initial_value: impl FnOnce() -> T) -> Rc(initial_value: impl FnOnce() -> T) -> Rc { - use_hook( - || Rc::new(initial_value()), - |state, _| Rc::clone(state), - |_| {}, - ) -} - /// This hook is used for obtaining a [`NodeRef`]. /// It persists across renders. /// diff --git a/packages/yew/tests/use_memo.rs b/packages/yew/tests/use_memo.rs new file mode 100644 index 00000000000..7dd39aac64b --- /dev/null +++ b/packages/yew/tests/use_memo.rs @@ -0,0 +1,53 @@ +use std::sync::atomic::{AtomicBool, Ordering}; + +mod common; + +use common::obtain_result; +use wasm_bindgen_test::*; +use yew::prelude::*; + +wasm_bindgen_test::wasm_bindgen_test_configure!(run_in_browser); + +#[wasm_bindgen_test] +fn use_memo_works() { + #[function_component(UseMemoComponent)] + fn use_memo_comp() -> Html { + let state = use_state(|| 0); + + let memoed_val = use_memo( + |_| { + static CTR: AtomicBool = AtomicBool::new(false); + + if CTR.swap(true, Ordering::Relaxed) { + panic!("multiple times rendered!"); + } + + "true" + }, + (), + ); + + use_effect(move || { + if *state < 5 { + state.set(*state + 1); + } + + || {} + }); + + html! { +
+ {"The test output is: "} +
{*memoed_val}
+ {"\n"} +
+ } + } + + yew::start_app_in_element::( + gloo_utils::document().get_element_by_id("output").unwrap(), + ); + + let result = obtain_result(); + assert_eq!(result.as_str(), "true"); +} From ffca655f091c03c243cb68064ae02ecb13ac52a3 Mon Sep 17 00:00:00 2001 From: Kaede Hoshikawa Date: Sat, 22 Jan 2022 21:17:57 +0900 Subject: [PATCH 19/42] Optimise Implementation of hooks. --- packages/yew/src/functional/hooks/use_context.rs | 16 ++++++++++------ packages/yew/src/functional/hooks/use_ref.rs | 10 +++------- 2 files changed, 13 insertions(+), 13 deletions(-) diff --git a/packages/yew/src/functional/hooks/use_context.rs b/packages/yew/src/functional/hooks/use_context.rs index 29f75597e86..b013a9c3bb8 100644 --- a/packages/yew/src/functional/hooks/use_context.rs +++ b/packages/yew/src/functional/hooks/use_context.rs @@ -1,6 +1,6 @@ use crate::callback::Callback; use crate::context::ContextHandle; -use crate::functional::{hook, use_component_scope, use_state}; +use crate::functional::{hook, use_component_scope, use_memo, use_state}; /// Hook for consuming context values in function components. /// The context of the type passed as `T` is returned. If there is no such context in scope, `None` is returned. @@ -49,13 +49,17 @@ pub fn use_context() -> Option { let val = use_state(|| -> Option { None }); let state = { let val_dispatcher = val.setter(); - use_state(move || State { - context: scope.context::(Callback::from(move |m| { - val_dispatcher.clone().set(Some(m)); - })), - }) + use_memo( + move |_| State { + context: scope.context::(Callback::from(move |m| { + val_dispatcher.clone().set(Some(m)); + })), + }, + (), + ) }; + // we fallback to initial value if it was not overriden. (*val) .clone() .or_else(move || state.context.as_ref().map(|m| m.0.clone())) diff --git a/packages/yew/src/functional/hooks/use_ref.rs b/packages/yew/src/functional/hooks/use_ref.rs index 76e1f087d46..7fc847a7fa8 100644 --- a/packages/yew/src/functional/hooks/use_ref.rs +++ b/packages/yew/src/functional/hooks/use_ref.rs @@ -1,7 +1,7 @@ use std::cell::RefCell; use std::rc::Rc; -use crate::functional::{hook, use_hook}; +use crate::functional::{hook, use_memo}; use crate::NodeRef; /// This hook is used for obtaining a mutable reference to a stateful value. @@ -52,11 +52,7 @@ use crate::NodeRef; /// ``` #[hook] pub fn use_mut_ref(initial_value: impl FnOnce() -> T) -> Rc> { - use_hook( - || Rc::new(RefCell::new(initial_value())), - |state, _| state.clone(), - |_| {}, - ) + use_memo(|_| RefCell::new(initial_value()), ()) } /// This hook is used for obtaining a [`NodeRef`]. @@ -118,5 +114,5 @@ pub fn use_mut_ref(initial_value: impl FnOnce() -> T) -> Rc NodeRef { - use_hook(NodeRef::default, |state, _| state.clone(), |_| {}) + (*use_memo(|_| NodeRef::default(), ())).clone() } From 7b8978bc7cabeb0ddd09076df4291106d69158ce Mon Sep 17 00:00:00 2001 From: Kaede Hoshikawa Date: Sat, 22 Jan 2022 21:41:41 +0900 Subject: [PATCH 20/42] Use Box to capture function value only. --- packages/yew-macro/src/hook/mod.rs | 16 +++++------ packages/yew-macro/src/hook/signature.rs | 34 ++++++++++++------------ 2 files changed, 25 insertions(+), 25 deletions(-) diff --git a/packages/yew-macro/src/hook/mod.rs b/packages/yew-macro/src/hook/mod.rs index 8f5c8b956f3..43b673121f3 100644 --- a/packages/yew-macro/src/hook/mod.rs +++ b/packages/yew-macro/src/hook/mod.rs @@ -113,8 +113,8 @@ When used in function components and hooks, this hook is equivalent to: let hook_lifetime_plus = quote! { #hook_lifetime + }; let inner_ident = Ident::new("inner", Span::mixed_site()); - // let inner_fn_ident = Ident::new("inner_fn", Span::mixed_site()); - // let input_args = hook_sig.input_args(); + let inner_fn_ident = Ident::new("inner_fn", Span::mixed_site()); + let input_args = hook_sig.input_args(); let boxed_fn_rt = match &sig.output { ReturnType::Default => None, @@ -126,15 +126,15 @@ When used in function components and hooks, this hook is equivalent to: #(#attrs)* #[doc = #doc_text] #vis #fn_token #ident #generics (#inputs) #hook_return_type #where_clause { - // fn #inner_fn_ident #generics (#ctx_ident: &mut ::yew::functional::HookContext, #inputs) -> #output_type #block + fn #inner_fn_ident #generics (#ctx_ident: &mut ::yew::functional::HookContext, #inputs) #boxed_fn_rt #where_clause #block // always capture inputs with closure for now, we need boxing implementation for `impl Trait` // arguments anyways. - // let inner = ::std::boxed::Box::new(move |#ctx_ident: &mut ::yew::functional::HookContext| #inner_fn_ident #call_generics (#ctx_ident, #(#input_args)*) ) - // as ::std::boxed::Box<#hook_lifetime_plus FnOnce(&mut ::yew::functional::HookContext) -> #output_type>; - - let #inner_ident = ::std::boxed::Box::new(move |#ctx_ident: &mut ::yew::functional::HookContext| #boxed_fn_rt #block ) - as #boxed_fn_type; + let #inner_ident = ::std::boxed::Box::new( + move |#ctx_ident: &mut ::yew::functional::HookContext| #boxed_fn_rt { + #inner_fn_ident (#ctx_ident, #(#input_args,)*) + } + ) as #boxed_fn_type; struct #hook_struct_name #generics #where_clause { _marker: ::std::marker::PhantomData<( #(#phantom_types,)* #(#phantom_lifetimes,)* )>, diff --git a/packages/yew-macro/src/hook/signature.rs b/packages/yew-macro/src/hook/signature.rs index 737d153a51c..eed04f530bb 100644 --- a/packages/yew-macro/src/hook/signature.rs +++ b/packages/yew-macro/src/hook/signature.rs @@ -3,8 +3,8 @@ use quote::quote; use syn::spanned::Spanned; use syn::visit_mut::VisitMut; use syn::{ - parse_quote, parse_quote_spanned, token, FnArg, Ident, Lifetime, ReturnType, Signature, Type, - TypeReference, WhereClause, + parse_quote, parse_quote_spanned, token, FnArg, Ident, Lifetime, Pat, ReturnType, Signature, + Type, TypeReference, WhereClause, }; use super::lifetime; @@ -120,19 +120,19 @@ impl HookSignature { .collect() } - // pub fn input_args(&self) -> Vec { - // self.sig - // .inputs - // .iter() - // .filter_map(|m| { - // if let FnArg::Typed(m) = m { - // if let Pat::Ident(ref m) = *m.pat { - // return Some(m.ident.clone()); - // } - // } - - // None - // }) - // .collect() - // } + pub fn input_args(&self) -> Vec { + self.sig + .inputs + .iter() + .filter_map(|m| { + if let FnArg::Typed(m) = m { + if let Pat::Ident(ref m) = *m.pat { + return Some(m.ident.clone()); + } + } + + None + }) + .collect() + } } From 15585ff6622406c8f4ff9f599b7d232be0164bdf Mon Sep 17 00:00:00 2001 From: Kaede Hoshikawa Date: Sat, 22 Jan 2022 22:07:22 +0900 Subject: [PATCH 21/42] Detect whether needs boxing. --- packages/yew-macro/src/hook/signature.rs | 28 ++++++++++++++++++++++-- 1 file changed, 26 insertions(+), 2 deletions(-) diff --git a/packages/yew-macro/src/hook/signature.rs b/packages/yew-macro/src/hook/signature.rs index eed04f530bb..16e99205796 100644 --- a/packages/yew-macro/src/hook/signature.rs +++ b/packages/yew-macro/src/hook/signature.rs @@ -3,16 +3,36 @@ use quote::quote; use syn::spanned::Spanned; use syn::visit_mut::VisitMut; use syn::{ - parse_quote, parse_quote_spanned, token, FnArg, Ident, Lifetime, Pat, ReturnType, Signature, - Type, TypeReference, WhereClause, + parse_quote, parse_quote_spanned, token, visit_mut, FnArg, Ident, Lifetime, Pat, ReturnType, + Signature, Type, TypeImplTrait, TypeReference, WhereClause, }; use super::lifetime; +#[derive(Default)] +pub struct CollectArgs { + needs_boxing: bool, +} + +impl CollectArgs { + pub fn new() -> Self { + Self::default() + } +} + +impl VisitMut for CollectArgs { + fn visit_type_impl_trait_mut(&mut self, impl_trait: &mut TypeImplTrait) { + self.needs_boxing = true; + + visit_mut::visit_type_impl_trait_mut(self, impl_trait); + } +} + pub struct HookSignature { pub hook_lifetime: Lifetime, pub sig: Signature, pub output_type: Type, + pub needs_boxing: bool, } impl HookSignature { @@ -37,6 +57,9 @@ impl HookSignature { pub fn rewrite(sig: &Signature) -> Self { let mut sig = sig.clone(); + let mut arg_info = CollectArgs::new(); + arg_info.visit_signature_mut(&mut sig); + let mut lifetimes = lifetime::CollectLifetimes::new("'arg", sig.ident.span()); for arg in sig.inputs.iter_mut() { match arg { @@ -101,6 +124,7 @@ impl HookSignature { hook_lifetime, sig, output_type, + needs_boxing: arg_info.needs_boxing, } } From f07c648fe5dfaffc2ea4774cc58232019e5c2f16 Mon Sep 17 00:00:00 2001 From: Kaede Hoshikawa Date: Sun, 23 Jan 2022 09:30:16 +0900 Subject: [PATCH 22/42] Add args if boxing not needed. --- packages/yew-macro/src/hook/mod.rs | 62 +++++++++++++++++++----- packages/yew-macro/src/hook/signature.rs | 25 +++++++++- 2 files changed, 74 insertions(+), 13 deletions(-) diff --git a/packages/yew-macro/src/hook/mod.rs b/packages/yew-macro/src/hook/mod.rs index 43b673121f3..1d5915898ae 100644 --- a/packages/yew-macro/src/hook/mod.rs +++ b/packages/yew-macro/src/hook/mod.rs @@ -116,22 +116,21 @@ When used in function components and hooks, this hook is equivalent to: let inner_fn_ident = Ident::new("inner_fn", Span::mixed_site()); let input_args = hook_sig.input_args(); - let boxed_fn_rt = match &sig.output { + // there might be some overridden lifetimes in the return type. + let inner_fn_rt = match &sig.output { ReturnType::Default => None, - ReturnType::Type(_, _) => Some(quote! { -> #output_type }), + ReturnType::Type(rarrow, _) => Some(quote! { #rarrow #output_type }), }; - let boxed_fn_type = quote! { ::std::boxed::Box }; - let output = quote! { - #(#attrs)* - #[doc = #doc_text] - #vis #fn_token #ident #generics (#inputs) #hook_return_type #where_clause { - fn #inner_fn_ident #generics (#ctx_ident: &mut ::yew::functional::HookContext, #inputs) #boxed_fn_rt #where_clause #block + let inner_fn = quote! { fn #inner_fn_ident #generics (#ctx_ident: &mut ::yew::functional::HookContext, #inputs) #inner_fn_rt #where_clause #block }; + + let inner_type_impl = if hook_sig.needs_boxing { + let boxed_fn_type = quote! { ::std::boxed::Box }; - // always capture inputs with closure for now, we need boxing implementation for `impl Trait` - // arguments anyways. + // We need boxing implementation for `impl Trait` arguments. + quote! { let #inner_ident = ::std::boxed::Box::new( - move |#ctx_ident: &mut ::yew::functional::HookContext| #boxed_fn_rt { + move |#ctx_ident: &mut ::yew::functional::HookContext| #inner_fn_rt { #inner_fn_ident (#ctx_ident, #(#input_args,)*) } ) as #boxed_fn_type; @@ -160,6 +159,47 @@ When used in function components and hooks, this hook is equivalent to: #hook_struct_name #call_generics ::new(#inner_ident) } + } else { + let input_types = hook_sig.input_types(); + let args_ident = Ident::new("args", Span::mixed_site()); + + quote! { + struct #hook_struct_name #generics #where_clause { + _marker: ::std::marker::PhantomData<( #(#phantom_types,)* #(#phantom_lifetimes,)* )>, + #args_ident: (#(#input_types,)*), + } + + impl #impl_generics ::yew::functional::Hook for #hook_struct_name #ty_generics #where_clause { + type Output = #output_type; + + fn run(mut self, #ctx_ident: &mut ::yew::functional::HookContext) -> Self::Output { + let (#(#input_args,)*) = self.#args_ident; + + #inner_fn_ident(#ctx_ident, #(#input_args,)*) + } + } + + impl #impl_generics #hook_struct_name #ty_generics #where_clause { + fn new(#inputs) -> Self { + #hook_struct_name { + _marker: ::std::marker::PhantomData, + #args_ident: (#(#input_args,)*), + } + } + } + + #hook_struct_name #call_generics ::new(#(#input_args,)*) + } + }; + + let output = quote! { + #(#attrs)* + #[doc = #doc_text] + #vis #fn_token #ident #generics (#inputs) #hook_return_type #where_clause { + #inner_fn + + #inner_type_impl + } }; Ok(output) diff --git a/packages/yew-macro/src/hook/signature.rs b/packages/yew-macro/src/hook/signature.rs index 16e99205796..a2031585e2f 100644 --- a/packages/yew-macro/src/hook/signature.rs +++ b/packages/yew-macro/src/hook/signature.rs @@ -1,10 +1,11 @@ use proc_macro2::Span; +use proc_macro_error::emit_error; use quote::quote; use syn::spanned::Spanned; use syn::visit_mut::VisitMut; use syn::{ - parse_quote, parse_quote_spanned, token, visit_mut, FnArg, Ident, Lifetime, Pat, ReturnType, - Signature, Type, TypeImplTrait, TypeReference, WhereClause, + parse_quote, parse_quote_spanned, token, visit_mut, FnArg, Ident, Lifetime, Pat, Receiver, + ReturnType, Signature, Type, TypeImplTrait, TypeReference, WhereClause, }; use super::lifetime; @@ -26,6 +27,12 @@ impl VisitMut for CollectArgs { visit_mut::visit_type_impl_trait_mut(self, impl_trait); } + + fn visit_receiver_mut(&mut self, recv: &mut Receiver) { + emit_error!(recv, "methods cannot be hooks"); + + visit_mut::visit_receiver_mut(self, recv); + } } pub struct HookSignature { @@ -159,4 +166,18 @@ impl HookSignature { }) .collect() } + + pub fn input_types(&self) -> Vec { + self.sig + .inputs + .iter() + .filter_map(|m| { + if let FnArg::Typed(m) = m { + return Some(*m.ty.clone()); + } + + None + }) + .collect() + } } From 7118b197add598c4d9abbb8848e10ead17e55bd7 Mon Sep 17 00:00:00 2001 From: Kaede Hoshikawa Date: Sun, 23 Jan 2022 09:42:14 +0900 Subject: [PATCH 23/42] Enforce hook number. --- packages/yew/src/functional/mod.rs | 31 +++++++++++++++++-- .../yew/from-0_19_0-to-0_20_0.mdx | 7 +++++ 2 files changed, 36 insertions(+), 2 deletions(-) diff --git a/packages/yew/src/functional/mod.rs b/packages/yew/src/functional/mod.rs index 850e9298bb1..8c8a0186492 100644 --- a/packages/yew/src/functional/mod.rs +++ b/packages/yew/src/functional/mod.rs @@ -63,11 +63,14 @@ type ProcessMessage = Rc; /// A hook context to be passed to hooks. pub struct HookContext { - counter: usize, scope: AnyScope, + process_message: ProcessMessage, hooks: Vec>>, destroy_listeners: Vec>, + + counter: usize, + total_hook_counter: Option, } impl HookContext { @@ -170,6 +173,7 @@ where }, hooks: vec![], destroy_listeners: vec![], + total_hook_counter: 0, }), } } @@ -186,7 +190,30 @@ where let props = ctx.props(); let mut ctx = self.hook_ctx.borrow_mut(); ctx.counter = 0; - T::run(&mut *ctx, props) + + let result = T::run(&mut *ctx, props); + + // Procedural Macros catch most conditionally called hooks at compile time, but it cannot + // detect early return (as the return can be Err(_), Suspension). + if result.is_err() { + if let Some(m) = ctx.total_hook_counter { + // Suspended Components can have less hooks called when suspended, but not more. + if m < ctx.counter { + panic!("Hooks are called conditionally."); + } + } + } else { + match ctx.total_hook_counter { + Some(m) => { + if m != ctx.counter { + panic!("Hooks are called conditionally."); + } + } + None => { + ctx.total_hook_counter = Some(ctx.counter); + } + } + } } fn rendered(&mut self, ctx: &Context, _first_render: bool) { diff --git a/website/docs/migration-guides/yew/from-0_19_0-to-0_20_0.mdx b/website/docs/migration-guides/yew/from-0_19_0-to-0_20_0.mdx index 5a9292d5a28..a9f464ce2d4 100644 --- a/website/docs/migration-guides/yew/from-0_19_0-to-0_20_0.mdx +++ b/website/docs/migration-guides/yew/from-0_19_0-to-0_20_0.mdx @@ -6,3 +6,10 @@ title: "From 0.19.0 to 0.20.0" This method of controlling body has caused issues in event registration and SSR hydration. They have been removed. Read more in the [github issue](https://github.com/yewstack/yew/pull/2346). + +## Hooks and Function Components V2 + +The Function Components and Hooks API are re-implemented with a different mechanism: + +- User-defined hooks are now required to have a prefix `use_` and must be marked by `#[hook]` attribute. +- Hooks are now only callable under the top level of a function component or a user defined hooks. From 171e08574f3c1129413485a0ec8f847ea76696ff Mon Sep 17 00:00:00 2001 From: Kaede Hoshikawa Date: Sun, 23 Jan 2022 10:59:18 +0900 Subject: [PATCH 24/42] Deduplicate use_effect. --- packages/yew/src/functional/hooks/mod.rs | 7 +- .../yew/src/functional/hooks/use_context.rs | 9 -- .../yew/src/functional/hooks/use_effect.rs | 151 ++++++++---------- packages/yew/src/functional/mod.rs | 87 +++++----- 4 files changed, 109 insertions(+), 145 deletions(-) diff --git a/packages/yew/src/functional/hooks/mod.rs b/packages/yew/src/functional/hooks/mod.rs index b4805b7ceb0..fa5e867d4c8 100644 --- a/packages/yew/src/functional/hooks/mod.rs +++ b/packages/yew/src/functional/hooks/mod.rs @@ -71,12 +71,9 @@ where // Execute the actual hook closure we were given. Let it mutate the hook state and let // it create a callback that takes the mutable hook state. - let mut hook = updater.hook.borrow_mut(); - let hook: &mut T = hook - .downcast_mut() - .expect("Incompatible hook type. Hooks must always be called in the same order"); + let mut hook = updater.borrow_mut::(); - runner(hook, updater.clone()) + runner(&mut *hook, updater.clone()) } } diff --git a/packages/yew/src/functional/hooks/use_context.rs b/packages/yew/src/functional/hooks/use_context.rs index b013a9c3bb8..3e05f7aa925 100644 --- a/packages/yew/src/functional/hooks/use_context.rs +++ b/packages/yew/src/functional/hooks/use_context.rs @@ -35,15 +35,6 @@ pub fn use_context() -> Option { context: Option<(T, ContextHandle)>, } - impl PartialEq for State - where - T: Clone + PartialEq + 'static, - { - fn eq(&self, rhs: &Self) -> bool { - self.context.as_ref().map(|m| &m.0) == rhs.context.as_ref().map(|m| &m.0) - } - } - let scope = use_component_scope(); let val = use_state(|| -> Option { None }); diff --git a/packages/yew/src/functional/hooks/use_effect.rs b/packages/yew/src/functional/hooks/use_effect.rs index 15cf5295dcf..c3edf20b9b1 100644 --- a/packages/yew/src/functional/hooks/use_effect.rs +++ b/packages/yew/src/functional/hooks/use_effect.rs @@ -1,9 +1,62 @@ use crate::functional::{hook, use_hook}; use std::rc::Rc; -struct UseEffect { - runner: Option Destructor>>, - destructor: Option>, +type UseEffectRunnerFn = Box D>; + +struct UseEffectBase { + runner_with_deps: Option<(Rc, UseEffectRunnerFn)>, + destructor: Option>, + deps: Option>, +} + +#[hook] +pub fn use_effect_base( + callback: impl FnOnce(&T) -> D + 'static, + deps: T, + should_render_fn: R, +) where + T: 'static, + D: FnOnce() + 'static, + R: FnOnce(Option<&T>, Option<&T>) -> bool + 'static, +{ + let deps = Rc::new(deps); + + use_hook( + move || { + let destructor: Option> = None; + UseEffectBase { + runner_with_deps: None, + destructor, + deps: None, + } + }, + move |state, updater| { + state.runner_with_deps = Some((deps, Box::new(callback))); + + updater.post_render(move |state: &mut UseEffectBase| { + if let Some((deps, callback)) = state.runner_with_deps.take() { + if !should_render_fn(Some(&*deps), state.deps.as_deref()) { + return false; + } + + if let Some(de) = state.destructor.take() { + de(); + } + + let new_destructor = callback(&deps); + + state.deps = Some(deps); + state.destructor = Some(Box::new(new_destructor)); + } + false + }); + }, + |hook| { + if let Some(destructor) = hook.destructor.take() { + destructor() + } + }, + ); } /// This hook is used for hooking into the component's lifecycle. @@ -37,51 +90,11 @@ struct UseEffect { /// } /// ``` #[hook] -pub fn use_effect(callback: impl FnOnce() -> Destructor + 'static) +pub fn use_effect(callback: impl FnOnce() -> D + 'static) where - Destructor: FnOnce() + 'static, + D: FnOnce() + 'static, { - use_hook( - move || { - let effect: UseEffect = UseEffect { - runner: None, - destructor: None, - }; - effect - }, - |state, updater| { - state.runner = Some(Box::new(callback) as Box Destructor>); - - // Run on every render - updater.post_render(move |state: &mut UseEffect| { - if let Some(callback) = state.runner.take() { - if let Some(de) = state.destructor.take() { - de(); - } - - let new_destructor = callback(); - state.destructor.replace(Box::new(new_destructor)); - } - false - }); - }, - |hook| { - if let Some(destructor) = hook.destructor.take() { - destructor() - } - }, - ) -} - -type UseEffectDepsRunnerFn = Box Destructor>; - -struct UseEffectDeps { - runner_with_deps: Option<( - Rc, - UseEffectDepsRunnerFn, - )>, - destructor: Option>, - deps: Option>, + use_effect_base(|_| callback(), (), |_, _| true); } /// This hook is similar to [`use_effect`] but it accepts dependencies. @@ -90,48 +103,10 @@ struct UseEffectDeps { /// To detect changes, dependencies must implement `PartialEq`. /// Note that the destructor also runs when dependencies change. #[hook] -pub fn use_effect_with_deps(callback: Callback, deps: Dependents) +pub fn use_effect_with_deps(callback: impl FnOnce(&T) -> D + 'static, deps: T) where - Callback: FnOnce(&Dependents) -> Destructor + 'static, - Destructor: FnOnce() + 'static, - Dependents: PartialEq + 'static, + T: PartialEq + 'static, + D: FnOnce() + 'static, { - let deps = Rc::new(deps); - - use_hook( - move || { - let destructor: Option> = None; - UseEffectDeps { - runner_with_deps: None, - destructor, - deps: None, - } - }, - move |state, updater| { - state.runner_with_deps = Some((deps, Box::new(callback))); - - updater.post_render(move |state: &mut UseEffectDeps| { - if let Some((deps, callback)) = state.runner_with_deps.take() { - if Some(&deps) == state.deps.as_ref() { - return false; - } - - if let Some(de) = state.destructor.take() { - de(); - } - - let new_destructor = callback(&deps); - - state.deps = Some(deps); - state.destructor = Some(Box::new(new_destructor)); - } - false - }); - }, - |hook| { - if let Some(destructor) = hook.destructor.take() { - destructor() - } - }, - ); + use_effect_base(callback, deps, |lhs, rhs| lhs == rhs) } diff --git a/packages/yew/src/functional/mod.rs b/packages/yew/src/functional/mod.rs index 8c8a0186492..d34aa819f1b 100644 --- a/packages/yew/src/functional/mod.rs +++ b/packages/yew/src/functional/mod.rs @@ -15,7 +15,7 @@ use crate::html::{AnyScope, BaseComponent, HtmlResult}; use crate::Properties; -use std::cell::RefCell; +use std::cell::{RefCell, RefMut}; use std::fmt; use std::ops::DerefMut; use std::rc::Rc; @@ -88,23 +88,25 @@ impl HookContext { let hook_pos = self.counter; self.counter += 1; - // Initialize hook if this is the first call - if hook_pos >= self.hooks.len() { - let initial_state = Rc::new(RefCell::new(initializer())); - self.hooks.push(initial_state.clone()); - self.destroy_listeners.push(Box::new(move || { - destructor(initial_state.borrow_mut().deref_mut()); - })); - } - - let hook = self - .hooks - .get(hook_pos) - .expect("Not the same number of hooks. Hooks must not be called conditionally") - .clone(); + let hook = match self.hooks.get(hook_pos).cloned() { + Some(m) => m, + None => { + let initial_state = Rc::new(RefCell::new(initializer())); + self.hooks.push(initial_state.clone()); + + { + let initial_state = initial_state.clone(); + self.destroy_listeners.push(Box::new(move || { + destructor(initial_state.borrow_mut().deref_mut()); + })); + } + initial_state + } + }; HookUpdater { hook, + scope: self.scope.clone(), process_message: self.process_message.clone(), } } @@ -173,7 +175,7 @@ where }, hooks: vec![], destroy_listeners: vec![], - total_hook_counter: 0, + total_hook_counter: None, }), } } @@ -193,7 +195,7 @@ where let result = T::run(&mut *ctx, props); - // Procedural Macros catch most conditionally called hooks at compile time, but it cannot + // Procedural Macros can catch most conditionally called hooks at compile time, but it cannot // detect early return (as the return can be Err(_), Suspension). if result.is_err() { if let Some(m) = ctx.total_hook_counter { @@ -214,6 +216,8 @@ where } } } + + result } fn rendered(&mut self, ctx: &Context, _first_render: bool) { @@ -247,64 +251,61 @@ impl MsgQueue { /// The `HookUpdater` provides a convenient interface for hooking into the lifecycle of /// the underlying Yew Component that backs the function component. -/// -/// Two interfaces are provided - callback and post_render. -/// - `callback` allows the creation of regular yew callbacks on the host component. -/// - `post_render` allows the creation of events that happen after a render is complete. -/// -/// See [`use_effect`](hooks::use_effect()) and [`use_context`](hooks::use_context()) -/// for more details on how to use the hook updater to provide function components -/// the necessary callbacks to update the underlying state. #[derive(Clone)] pub(crate) struct HookUpdater { hook: Rc>, + scope: AnyScope, process_message: ProcessMessage, } impl HookUpdater { - /// Callback which runs the hook. + /// Retrieves the hook state + pub fn borrow_mut(&self) -> RefMut<'_, T> { + RefMut::map(self.hook.borrow_mut(), |m| { + m.downcast_mut() + .expect("incompatible hook type. Hooks must always be called in the same order") + }) + } + + /// Registers a callback to be run immediately. pub fn callback(&self, cb: F) where F: FnOnce(&mut T) -> bool + 'static, { - let internal_hook_state = self.hook.clone(); - let process_message = self.process_message.clone(); + let this = self.clone(); // Update the component // We're calling "link.send_message", so we're not calling it post-render let post_render = false; - process_message( + (self.process_message)( Box::new(move || { - let mut r = internal_hook_state.borrow_mut(); - let hook: &mut T = r - .downcast_mut() - .expect("internal error: hook downcasted to wrong type"); - cb(hook) + let mut hook = this.borrow_mut(); + cb(&mut *hook) }), post_render, ); } - /// Callback called after the render + /// Registers a callback to be run after the render pub fn post_render(&self, cb: F) where F: FnOnce(&mut T) -> bool + 'static, { - let internal_hook_state = self.hook.clone(); - let process_message = self.process_message.clone(); + let this = self.clone(); // Update the component // We're calling "message_queue.push", so not calling it post-render let post_render = true; - process_message( + (self.process_message)( Box::new(move || { - let mut hook = internal_hook_state.borrow_mut(); - let hook: &mut T = hook - .downcast_mut() - .expect("internal error: hook downcasted to wrong type"); - cb(hook) + let mut hook = this.borrow_mut(); + cb(&mut *hook) }), post_render, ); } + + pub fn scope(&self) -> &AnyScope { + &self.scope + } } From 2f3856a588f60473a432e4ab4ac63b81ad6c5a59 Mon Sep 17 00:00:00 2001 From: Kaede Hoshikawa Date: Sun, 23 Jan 2022 11:14:24 +0900 Subject: [PATCH 25/42] Optimise Implementation. --- packages/yew/src/functional/hooks/mod.rs | 12 +----- .../yew/src/functional/hooks/use_context.rs | 2 +- packages/yew/src/functional/hooks/use_memo.rs | 37 +++++-------------- 3 files changed, 12 insertions(+), 39 deletions(-) diff --git a/packages/yew/src/functional/hooks/mod.rs b/packages/yew/src/functional/hooks/mod.rs index fa5e867d4c8..4a197b898ce 100644 --- a/packages/yew/src/functional/hooks/mod.rs +++ b/packages/yew/src/functional/hooks/mod.rs @@ -85,15 +85,5 @@ where } pub(crate) fn use_component_scope() -> impl Hook { - struct HookProvider {} - - impl Hook for HookProvider { - type Output = AnyScope; - - fn run(self, ctx: &mut HookContext) -> Self::Output { - ctx.scope().clone() - } - } - - HookProvider {} + use_hook(|| (), |_, updater| updater.scope().clone(), |_| {}) } diff --git a/packages/yew/src/functional/hooks/use_context.rs b/packages/yew/src/functional/hooks/use_context.rs index 3e05f7aa925..0a2489c9761 100644 --- a/packages/yew/src/functional/hooks/use_context.rs +++ b/packages/yew/src/functional/hooks/use_context.rs @@ -50,7 +50,7 @@ pub fn use_context() -> Option { ) }; - // we fallback to initial value if it was not overriden. + // we fallback to initial value if it was not updated. (*val) .clone() .or_else(move || state.context.as_ref().map(|m| m.0.clone())) diff --git a/packages/yew/src/functional/hooks/use_memo.rs b/packages/yew/src/functional/hooks/use_memo.rs index 8151c1400df..70efa9ca7e2 100644 --- a/packages/yew/src/functional/hooks/use_memo.rs +++ b/packages/yew/src/functional/hooks/use_memo.rs @@ -16,40 +16,23 @@ where T: 'static, Dependents: 'static + PartialEq, { - deps: Option>, - val: Option>, + inner: Option<(Rc, Rc)>, } - use_hook( - || { - let state: UseMemo = UseMemo { - val: None, - deps: None, - }; - + use_hook::, _, _, _, _>( + || UseMemo { inner: None }, + move |state, _updater| { state - }, - move |state, _updater| match state.val.clone() { - Some(m) => { - if Some(&deps) != state.deps.as_ref() { + .inner + .as_ref() + .and_then(|(m, n)| (m.as_ref() == &*deps).then(|| n.clone())) + .unwrap_or_else(|| { let val = Rc::new(memo_fn(&deps)); - state.val = Some(val.clone()); - state.deps = Some(deps); + state.inner = Some((deps, val.clone())); val - } else { - m - } - } - None => { - let val = Rc::new(memo_fn(&deps)); - - state.val = Some(val.clone()); - state.deps = Some(deps); - - val - } + }) }, |_| {}, ) From 2b1d6f191ad1c1cc3f78819d48e1556908cf0979 Mon Sep 17 00:00:00 2001 From: Kaede Hoshikawa Date: Sun, 23 Jan 2022 11:44:04 +0900 Subject: [PATCH 26/42] Update documentation. --- packages/yew/src/functional/hooks/use_effect.rs | 6 +++--- packages/yew/src/functional/mod.rs | 4 ---- website/docs/concepts/function-components/introduction.mdx | 5 +++-- 3 files changed, 6 insertions(+), 9 deletions(-) diff --git a/packages/yew/src/functional/hooks/use_effect.rs b/packages/yew/src/functional/hooks/use_effect.rs index c3edf20b9b1..f4f27a8e67b 100644 --- a/packages/yew/src/functional/hooks/use_effect.rs +++ b/packages/yew/src/functional/hooks/use_effect.rs @@ -13,7 +13,7 @@ struct UseEffectBase { pub fn use_effect_base( callback: impl FnOnce(&T) -> D + 'static, deps: T, - should_render_fn: R, + effect_changed_fn: R, ) where T: 'static, D: FnOnce() + 'static, @@ -35,7 +35,7 @@ pub fn use_effect_base( updater.post_render(move |state: &mut UseEffectBase| { if let Some((deps, callback)) = state.runner_with_deps.take() { - if !should_render_fn(Some(&*deps), state.deps.as_deref()) { + if !effect_changed_fn(Some(&*deps), state.deps.as_deref()) { return false; } @@ -108,5 +108,5 @@ where T: PartialEq + 'static, D: FnOnce() + 'static, { - use_effect_base(callback, deps, |lhs, rhs| lhs == rhs) + use_effect_base(callback, deps, |lhs, rhs| lhs != rhs) } diff --git a/packages/yew/src/functional/mod.rs b/packages/yew/src/functional/mod.rs index d34aa819f1b..17285cacac7 100644 --- a/packages/yew/src/functional/mod.rs +++ b/packages/yew/src/functional/mod.rs @@ -110,10 +110,6 @@ impl HookContext { process_message: self.process_message.clone(), } } - - fn scope(&self) -> &AnyScope { - &self.scope - } } impl fmt::Debug for HookContext { diff --git a/website/docs/concepts/function-components/introduction.mdx b/website/docs/concepts/function-components/introduction.mdx index c75e0ae0213..008c9a49e1a 100644 --- a/website/docs/concepts/function-components/introduction.mdx +++ b/website/docs/concepts/function-components/introduction.mdx @@ -42,8 +42,9 @@ actions. Yew comes with a few pre-defined Hooks. You can also create your own. Hooks can only be used at the following locations: - Top level of a function / hook. -- If condition of a function / hook given it's not branched. -- Blocks inside a function / hook given it's not branched. +- If condition inside a function / hook, given it's not already branched. +- Match condition inside a function / hook, given it's not already branched. +- Blocks inside a function / hook, given it's not already branched. #### Pre-defined Hooks From ac765cb9f6434b067086fc8bb2771f7be1d38b15 Mon Sep 17 00:00:00 2001 From: Kaede Hoshikawa Date: Sun, 23 Jan 2022 12:10:16 +0900 Subject: [PATCH 27/42] Fix website test. Strip BoxedHook implementation from it. --- packages/yew-macro/src/hook/mod.rs | 46 ++++++------------- packages/yew/src/functional/hooks/mod.rs | 26 ++++++++++- packages/yew/src/functional/mod.rs | 2 +- .../function-components/custom-hooks.mdx | 1 + .../function-components/introduction.mdx | 2 +- .../function-components/pre-defined-hooks.mdx | 16 +++---- 6 files changed, 47 insertions(+), 46 deletions(-) diff --git a/packages/yew-macro/src/hook/mod.rs b/packages/yew-macro/src/hook/mod.rs index 1d5915898ae..d591656ddab 100644 --- a/packages/yew-macro/src/hook/mod.rs +++ b/packages/yew-macro/src/hook/mod.rs @@ -95,24 +95,15 @@ When used in function components and hooks, this hook is equivalent to: .. } = hook_sig.sig; - let hook_lifetime = &hook_sig.hook_lifetime; let output_type = &hook_sig.output_type; - let hook_struct_name = Ident::new("HookProvider", Span::mixed_site()); let (impl_generics, ty_generics, where_clause) = generics.split_for_impl(); - let call_generics = ty_generics.as_turbofish(); let ctx_ident = Ident::new("ctx", Span::mixed_site()); - let phantom_types = hook_sig.phantom_types(); - let phantom_lifetimes = hook_sig.phantom_lifetimes(); - let mut body_rewriter = BodyRewriter::default(); visit_mut::visit_block_mut(&mut body_rewriter, &mut *block); - let hook_lifetime_plus = quote! { #hook_lifetime + }; - let inner_ident = Ident::new("inner", Span::mixed_site()); - let inner_fn_ident = Ident::new("inner_fn", Span::mixed_site()); let input_args = hook_sig.input_args(); @@ -125,43 +116,32 @@ When used in function components and hooks, this hook is equivalent to: let inner_fn = quote! { fn #inner_fn_ident #generics (#ctx_ident: &mut ::yew::functional::HookContext, #inputs) #inner_fn_rt #where_clause #block }; let inner_type_impl = if hook_sig.needs_boxing { + let hook_lifetime = &hook_sig.hook_lifetime; + let hook_lifetime_plus = quote! { #hook_lifetime + }; + + let boxed_inner_ident = Ident::new("boxed_inner", Span::mixed_site()); let boxed_fn_type = quote! { ::std::boxed::Box }; // We need boxing implementation for `impl Trait` arguments. quote! { - let #inner_ident = ::std::boxed::Box::new( + let #boxed_inner_ident = ::std::boxed::Box::new( move |#ctx_ident: &mut ::yew::functional::HookContext| #inner_fn_rt { #inner_fn_ident (#ctx_ident, #(#input_args,)*) } ) as #boxed_fn_type; - struct #hook_struct_name #generics #where_clause { - _marker: ::std::marker::PhantomData<( #(#phantom_types,)* #(#phantom_lifetimes,)* )>, - #inner_ident: #boxed_fn_type, - } - - impl #impl_generics ::yew::functional::Hook for #hook_struct_name #ty_generics #where_clause { - type Output = #output_type; - - fn run(mut self, #ctx_ident: &mut ::yew::functional::HookContext) -> Self::Output { - (self.inner)(#ctx_ident) - } - } - - impl #impl_generics #hook_struct_name #ty_generics #where_clause { - fn new(#inner_ident: #boxed_fn_type) -> Self { - #hook_struct_name { - _marker: ::std::marker::PhantomData, - #inner_ident, - } - } - } - - #hook_struct_name #call_generics ::new(#inner_ident) + ::yew::functional::BoxedHook::<#hook_lifetime, #output_type>::new(#boxed_inner_ident) } } else { let input_types = hook_sig.input_types(); + let args_ident = Ident::new("args", Span::mixed_site()); + let hook_struct_name = Ident::new("HookProvider", Span::mixed_site()); + + let call_generics = ty_generics.as_turbofish(); + + let phantom_types = hook_sig.phantom_types(); + let phantom_lifetimes = hook_sig.phantom_lifetimes(); quote! { struct #hook_struct_name #generics #where_clause { diff --git a/packages/yew/src/functional/hooks/mod.rs b/packages/yew/src/functional/hooks/mod.rs index 4a197b898ce..f57ecda98d4 100644 --- a/packages/yew/src/functional/hooks/mod.rs +++ b/packages/yew/src/functional/hooks/mod.rs @@ -12,7 +12,7 @@ pub use use_reducer::*; pub use use_ref::*; pub use use_state::*; -use crate::functional::{AnyScope, HookContext, HookUpdater}; +use crate::functional::{hook, AnyScope, HookContext, HookUpdater}; /// A trait that is implemented on hooks. /// @@ -26,6 +26,27 @@ pub trait Hook { fn run(self, ctx: &mut HookContext) -> Self::Output; } +/// The blanket implementation of boxed hooks. +#[doc(hidden)] +#[allow(missing_debug_implementations, missing_docs)] +pub struct BoxedHook<'hook, T> { + inner: Box T>, +} + +impl<'hook, T> BoxedHook<'hook, T> { + pub fn new(inner: Box T>) -> Self { + Self { inner } + } +} + +impl Hook for BoxedHook<'_, T> { + type Output = T; + + fn run(self, ctx: &mut HookContext) -> Self::Output { + (self.inner)(ctx) + } +} + /// Low level building block of creating hooks. /// /// It is used to created the pre-defined primitive hooks. @@ -84,6 +105,7 @@ where } } -pub(crate) fn use_component_scope() -> impl Hook { +#[hook] +pub(crate) fn use_component_scope() -> AnyScope { use_hook(|| (), |_, updater| updater.scope().clone(), |_| {}) } diff --git a/packages/yew/src/functional/mod.rs b/packages/yew/src/functional/mod.rs index 17285cacac7..627942e0a5d 100644 --- a/packages/yew/src/functional/mod.rs +++ b/packages/yew/src/functional/mod.rs @@ -55,7 +55,7 @@ use crate::html::SealedBaseComponent; /// ``` pub use yew_macro::function_component; -/// This attribute creates a hook from a normal Rust function. +/// This attribute creates a user-defined hook from a normal Rust function. pub use yew_macro::hook; type Msg = Box bool>; diff --git a/website/docs/concepts/function-components/custom-hooks.mdx b/website/docs/concepts/function-components/custom-hooks.mdx index fd1a39cb79a..95445b1b56f 100644 --- a/website/docs/concepts/function-components/custom-hooks.mdx +++ b/website/docs/concepts/function-components/custom-hooks.mdx @@ -46,6 +46,7 @@ All hooks must be marked by `#[hook]` to function as as hook. use web_sys::{Event, EventTarget}; use std::borrow::Cow; use gloo::events::EventListener; +use yew::prelude::*; #[hook] pub fn use_event(target: &EventTarget, event_type: E, callback: F) diff --git a/website/docs/concepts/function-components/introduction.mdx b/website/docs/concepts/function-components/introduction.mdx index 008c9a49e1a..64098c07238 100644 --- a/website/docs/concepts/function-components/introduction.mdx +++ b/website/docs/concepts/function-components/introduction.mdx @@ -51,7 +51,7 @@ Hooks can only be used at the following locations: Yew comes with the following predefined Hooks: - [`use_state`](./../function-components/pre-defined-hooks.mdx#use_state) - [`use_state_eq`](./../function-components/pre-defined-hooks.mdx#use_state_eq) -- [`use_ref`](./../function-components/pre-defined-hooks.mdx#use_ref) +- [`use_memo`](./../function-components/pre-defined-hooks.mdx#use_memo) - [`use_mut_ref`](./../function-components/pre-defined-hooks.mdx#use_mut_ref) - [`use_node_ref`](./../function-components/pre-defined-hooks.mdx#use_node_ref) - [`use_reducer`](./../function-components/pre-defined-hooks.mdx#use_reducer) diff --git a/website/docs/concepts/function-components/pre-defined-hooks.mdx b/website/docs/concepts/function-components/pre-defined-hooks.mdx index 0c800dfbce4..995097b9980 100644 --- a/website/docs/concepts/function-components/pre-defined-hooks.mdx +++ b/website/docs/concepts/function-components/pre-defined-hooks.mdx @@ -64,22 +64,20 @@ re-render when the setter receives a value that `prev_state != next_state`. This hook requires the state object to implement `PartialEq`. -## `use_ref` -`use_ref` is used for obtaining an immutable reference to a value. +## `use_memo` +`use_memo` is used for obtaining an immutable reference to a value. Its state persists across renders. +Its value will be recalculated if the dependency changes. -`use_ref` can be useful for keeping things in scope for the lifetime of the component, so long as +`use_memo` can be useful for keeping things in scope for the lifetime of the component, so long as you don't store a clone of the resulting `Rc` anywhere that outlives the component. -If you need a mutable reference, consider using [`use_mut_ref`](#use_mut_ref). -If you need the component to be re-rendered on state change, consider using [`use_state`](#use_state). - ```rust -use yew::{function_component, html, use_ref, use_state, Callback, Html}; +use yew::{function_component, html, use_memo, use_state, Callback, Html}; -#[function_component(UseRef)] +#[function_component(UseMemo)] fn ref_hook() -> Html { - let message = use_ref(|| "Some Expensive State.".to_string()); + let message = use_memo(|_| "Some Expensive State.".to_string(), ()); html! {
From 9c300ed1b5fd127b6738f5a7e7270645c4641d6c Mon Sep 17 00:00:00 2001 From: Kaede Hoshikawa Date: Sun, 23 Jan 2022 12:17:21 +0900 Subject: [PATCH 28/42] Allow doc string. --- packages/yew/src/functional/hooks/mod.rs | 1 + 1 file changed, 1 insertion(+) diff --git a/packages/yew/src/functional/hooks/mod.rs b/packages/yew/src/functional/hooks/mod.rs index f57ecda98d4..860a7a9646c 100644 --- a/packages/yew/src/functional/hooks/mod.rs +++ b/packages/yew/src/functional/hooks/mod.rs @@ -34,6 +34,7 @@ pub struct BoxedHook<'hook, T> { } impl<'hook, T> BoxedHook<'hook, T> { + #[allow(missing_docs)] pub fn new(inner: Box T>) -> Self { Self { inner } } From 76da3f6ec18973df0751b95988600d43861d190e Mon Sep 17 00:00:00 2001 From: Kaede Hoshikawa Date: Sun, 23 Jan 2022 12:46:17 +0900 Subject: [PATCH 29/42] Workaround doc tests. --- packages/yew-macro/src/hook/mod.rs | 10 ++++++++-- packages/yew/src/functional/hooks/use_effect.rs | 7 ++----- 2 files changed, 10 insertions(+), 7 deletions(-) diff --git a/packages/yew-macro/src/hook/mod.rs b/packages/yew-macro/src/hook/mod.rs index d591656ddab..cf0aa052235 100644 --- a/packages/yew-macro/src/hook/mod.rs +++ b/packages/yew-macro/src/hook/mod.rs @@ -48,14 +48,14 @@ impl Parse for HookFn { } pub fn hook_impl(component: HookFn) -> syn::Result { - let HookFn { inner } = component; + let HookFn { inner: original_fn } = component; let ItemFn { vis, sig, mut block, attrs, - } = inner; + } = original_fn.clone(); let sig_s = quote! { #vis #sig { __yew_macro_dummy_function_body__ @@ -172,7 +172,10 @@ When used in function components and hooks, this hook is equivalent to: } }; + // There're some weird issues with doc tests that it cannot detect return types properly. + // So we print original implementation instead. let output = quote! { + #[cfg(not(doctest))] #(#attrs)* #[doc = #doc_text] #vis #fn_token #ident #generics (#inputs) #hook_return_type #where_clause { @@ -180,6 +183,9 @@ When used in function components and hooks, this hook is equivalent to: #inner_type_impl } + + #[cfg(doctest)] + #original_fn }; Ok(output) diff --git a/packages/yew/src/functional/hooks/use_effect.rs b/packages/yew/src/functional/hooks/use_effect.rs index f4f27a8e67b..44b1916f48f 100644 --- a/packages/yew/src/functional/hooks/use_effect.rs +++ b/packages/yew/src/functional/hooks/use_effect.rs @@ -10,11 +10,8 @@ struct UseEffectBase { } #[hook] -pub fn use_effect_base( - callback: impl FnOnce(&T) -> D + 'static, - deps: T, - effect_changed_fn: R, -) where +fn use_effect_base(callback: impl FnOnce(&T) -> D + 'static, deps: T, effect_changed_fn: R) +where T: 'static, D: FnOnce() + 'static, R: FnOnce(Option<&T>, Option<&T>) -> bool + 'static, From 541a945b059ca65985bfef323e773f135784a71b Mon Sep 17 00:00:00 2001 From: Kaede Hoshikawa Date: Mon, 24 Jan 2022 21:48:18 +0900 Subject: [PATCH 30/42] Optimise codebase & documentation. --- packages/yew-macro/src/hook/body.rs | 12 ++-- packages/yew-macro/src/hook/lifetime.rs | 36 ++++++------ packages/yew-macro/src/hook/mod.rs | 2 +- packages/yew/src/functional/hooks/mod.rs | 13 ++--- packages/yew/src/functional/hooks/use_memo.rs | 9 +-- packages/yew/src/functional/mod.rs | 57 +++++++++---------- .../function-components/pre-defined-hooks.mdx | 17 ++++-- .../yew/from-0_19_0-to-0_20_0.mdx | 4 +- 8 files changed, 80 insertions(+), 70 deletions(-) diff --git a/packages/yew-macro/src/hook/body.rs b/packages/yew-macro/src/hook/body.rs index 344c2dff210..8cd2d0d3f07 100644 --- a/packages/yew-macro/src/hook/body.rs +++ b/packages/yew-macro/src/hook/body.rs @@ -1,5 +1,6 @@ use proc_macro2::Span; use proc_macro_error::emit_error; +use std::sync::{Arc, Mutex}; use syn::spanned::Spanned; use syn::visit_mut::VisitMut; use syn::{ @@ -9,22 +10,21 @@ use syn::{ #[derive(Debug, Default)] pub struct BodyRewriter { - branch_ctr: u64, + branch_lock: Arc>, } impl BodyRewriter { fn is_branched(&self) -> bool { - self.branch_ctr > 0 + self.branch_lock.try_lock().is_err() } fn with_branch(&mut self, f: F) -> O where F: FnOnce(&mut BodyRewriter) -> O, { - self.branch_ctr += 1; - let result = { f(self) }; - self.branch_ctr -= 1; - result + let branch_lock = self.branch_lock.clone(); + let _branched = branch_lock.try_lock(); + f(self) } } diff --git a/packages/yew-macro/src/hook/lifetime.rs b/packages/yew-macro/src/hook/lifetime.rs index 22b6c3b289a..9ea6c19fc86 100644 --- a/packages/yew-macro/src/hook/lifetime.rs +++ b/packages/yew-macro/src/hook/lifetime.rs @@ -1,7 +1,8 @@ use proc_macro2::Span; +use std::sync::{Arc, Mutex}; use syn::visit_mut::{self, VisitMut}; use syn::{ - GenericArgument, Lifetime, ParenthesizedGenericArguments, Receiver, TypeImplTrait, + GenericArgument, Lifetime, ParenthesizedGenericArguments, Receiver, TypeBareFn, TypeImplTrait, TypeParamBound, TypeReference, }; @@ -12,8 +13,8 @@ pub struct CollectLifetimes { pub name: &'static str, pub default_span: Span, - pub impl_trait_ctr: u64, - pub impl_fn_ctr: u64, + pub impl_trait_lock: Arc>, + pub impl_fn_lock: Arc>, } impl CollectLifetimes { @@ -24,17 +25,17 @@ impl CollectLifetimes { name, default_span, - impl_trait_ctr: 0, - impl_fn_ctr: 0, + impl_trait_lock: Arc::default(), + impl_fn_lock: Arc::default(), } } fn is_impl_trait(&self) -> bool { - self.impl_trait_ctr > 0 + self.impl_trait_lock.try_lock().is_err() } fn is_impl_fn(&self) -> bool { - self.impl_fn_ctr > 0 + self.impl_fn_lock.try_lock().is_err() } fn visit_opt_lifetime(&mut self, lifetime: &mut Option) { @@ -69,7 +70,7 @@ impl VisitMut for CollectLifetimes { } fn visit_type_reference_mut(&mut self, ty: &mut TypeReference) { - // We don't rewrite references in the impl FnOnce(&arg) + // We don't rewrite references in the impl FnOnce(&arg) or fn(&arg) if self.is_impl_fn() { return; } @@ -91,29 +92,30 @@ impl VisitMut for CollectLifetimes { } fn visit_type_impl_trait_mut(&mut self, impl_trait: &mut TypeImplTrait) { - self.impl_trait_ctr += 1; + let impl_trait_lock = self.impl_trait_lock.clone(); + let _locked = impl_trait_lock.try_lock(); impl_trait .bounds .insert(0, TypeParamBound::Lifetime(self.next_lifetime(None))); visit_mut::visit_type_impl_trait_mut(self, impl_trait); - - self.impl_trait_ctr -= 1; } fn visit_parenthesized_generic_arguments_mut( &mut self, generic_args: &mut ParenthesizedGenericArguments, ) { - if self.is_impl_trait() { - self.impl_fn_ctr += 1; - } + let impl_fn_lock = self.impl_fn_lock.clone(); + let _maybe_locked = self.is_impl_trait().then(|| impl_fn_lock.try_lock()); visit_mut::visit_parenthesized_generic_arguments_mut(self, generic_args); + } - if self.is_impl_trait() { - self.impl_fn_ctr -= 1; - } + fn visit_type_bare_fn_mut(&mut self, i: &mut TypeBareFn) { + let impl_fn_lock = self.impl_fn_lock.clone(); + let _locked = impl_fn_lock.try_lock(); + + visit_mut::visit_type_bare_fn_mut(self, i); } } diff --git a/packages/yew-macro/src/hook/mod.rs b/packages/yew-macro/src/hook/mod.rs index cf0aa052235..ab7f3744a02 100644 --- a/packages/yew-macro/src/hook/mod.rs +++ b/packages/yew-macro/src/hook/mod.rs @@ -24,7 +24,7 @@ impl Parse for HookFn { let sig = func.sig.clone(); if sig.asyncness.is_some() { - emit_error!(sig.asyncness, "hooks can't be async functions"); + emit_error!(sig.asyncness, "async functions can't be hooks"); } if sig.constness.is_some() { diff --git a/packages/yew/src/functional/hooks/mod.rs b/packages/yew/src/functional/hooks/mod.rs index 860a7a9646c..1de1b847b84 100644 --- a/packages/yew/src/functional/hooks/mod.rs +++ b/packages/yew/src/functional/hooks/mod.rs @@ -57,17 +57,14 @@ impl Hook for BoxedHook<'_, T> { /// The `initializer` callback is called once to create the initial state of the hook. /// `runner` callback handles the logic of the hook. It is called when the hook function is called. /// `destructor`, as the name implies, is called to cleanup the leftovers of the hook. -pub(crate) fn use_hook<'hook, T, INIT, RUN, TEAR, O>( - initializer: INIT, - runner: RUN, - destructor: TEAR, +pub(crate) fn use_hook<'hook, T, O>( + initializer: impl 'hook + FnOnce() -> T, + runner: impl 'hook + FnOnce(&mut T, HookUpdater) -> O, + destructor: impl 'static + FnOnce(&mut T), ) -> impl 'hook + Hook where T: 'static, O: 'hook, - INIT: 'hook + FnOnce() -> T, - RUN: 'hook + FnOnce(&mut T, HookUpdater) -> O, - TEAR: 'static + FnOnce(&mut T), { struct HookProvider<'a, T, O> { initializer: Box T + 'a>, @@ -89,7 +86,7 @@ where } = self; // Extract current hook - let updater = ctx.next_state::(initializer, destructor); + let updater = ctx.next_state(initializer, destructor); // Execute the actual hook closure we were given. Let it mutate the hook state and let // it create a callback that takes the mutable hook state. diff --git a/packages/yew/src/functional/hooks/use_memo.rs b/packages/yew/src/functional/hooks/use_memo.rs index 70efa9ca7e2..f37aa4f972d 100644 --- a/packages/yew/src/functional/hooks/use_memo.rs +++ b/packages/yew/src/functional/hooks/use_memo.rs @@ -1,8 +1,9 @@ use crate::functional::{hook, use_hook}; use std::rc::Rc; -/// This hook is used for obtaining a immutable reference to a value that is recalculated when it's -/// dependency changes. +/// Get a immutable reference to a memoized value +/// +/// Memoization means it will only get recalculated when provided dependencies update/change #[hook] pub fn use_memo(memo_fn: impl FnOnce(&Dependents) -> T, deps: Dependents) -> Rc where @@ -19,8 +20,8 @@ where inner: Option<(Rc, Rc)>, } - use_hook::, _, _, _, _>( - || UseMemo { inner: None }, + use_hook( + || -> UseMemo { UseMemo { inner: None } }, move |state, _updater| { state .inner diff --git a/packages/yew/src/functional/mod.rs b/packages/yew/src/functional/mod.rs index 627942e0a5d..1d88415961f 100644 --- a/packages/yew/src/functional/mod.rs +++ b/packages/yew/src/functional/mod.rs @@ -70,19 +70,18 @@ pub struct HookContext { destroy_listeners: Vec>, counter: usize, + #[cfg(debug_assertions)] total_hook_counter: Option, } impl HookContext { - pub(crate) fn next_state( + pub(crate) fn next_state( &mut self, - initializer: INIT, - destructor: TEAR, + initializer: impl FnOnce() -> T, + destructor: impl FnOnce(&mut T) + 'static, ) -> HookUpdater where T: 'static, - INIT: FnOnce() -> T, - TEAR: FnOnce(&mut T) + 'static, { // Determine which hook position we're at and increment for the next hook let hook_pos = self.counter; @@ -157,7 +156,6 @@ where _never: std::marker::PhantomData::default(), message_queue: message_queue.clone(), hook_ctx: RefCell::new(HookContext { - counter: 0, scope, process_message: { let scope = ctx.link().clone(); @@ -171,6 +169,9 @@ where }, hooks: vec![], destroy_listeners: vec![], + + counter: 0, + #[cfg(debug_assertions)] total_hook_counter: None, }), } @@ -189,26 +190,30 @@ where let mut ctx = self.hook_ctx.borrow_mut(); ctx.counter = 0; + #[allow(clippy::let_and_return)] let result = T::run(&mut *ctx, props); - // Procedural Macros can catch most conditionally called hooks at compile time, but it cannot - // detect early return (as the return can be Err(_), Suspension). - if result.is_err() { - if let Some(m) = ctx.total_hook_counter { - // Suspended Components can have less hooks called when suspended, but not more. - if m < ctx.counter { - panic!("Hooks are called conditionally."); - } - } - } else { - match ctx.total_hook_counter { - Some(m) => { - if m != ctx.counter { + #[cfg(debug_assertions)] + { + // Procedural Macros can catch most conditionally called hooks at compile time, but it cannot + // detect early return (as the return can be Err(_), Suspension). + if result.is_err() { + if let Some(m) = ctx.total_hook_counter { + // Suspended Components can have less hooks called when suspended, but not more. + if m < ctx.counter { panic!("Hooks are called conditionally."); } } - None => { - ctx.total_hook_counter = Some(ctx.counter); + } else { + match ctx.total_hook_counter { + Some(m) => { + if m != ctx.counter { + panic!("Hooks are called conditionally."); + } + } + None => { + ctx.total_hook_counter = Some(ctx.counter); + } } } } @@ -264,10 +269,7 @@ impl HookUpdater { } /// Registers a callback to be run immediately. - pub fn callback(&self, cb: F) - where - F: FnOnce(&mut T) -> bool + 'static, - { + pub fn callback(&self, cb: impl FnOnce(&mut T) -> bool + 'static) { let this = self.clone(); // Update the component @@ -283,10 +285,7 @@ impl HookUpdater { } /// Registers a callback to be run after the render - pub fn post_render(&self, cb: F) - where - F: FnOnce(&mut T) -> bool + 'static, - { + pub fn post_render(&self, cb: impl FnOnce(&mut T) -> bool + 'static) { let this = self.clone(); // Update the component diff --git a/website/docs/concepts/function-components/pre-defined-hooks.mdx b/website/docs/concepts/function-components/pre-defined-hooks.mdx index 995097b9980..043ee9f354a 100644 --- a/website/docs/concepts/function-components/pre-defined-hooks.mdx +++ b/website/docs/concepts/function-components/pre-defined-hooks.mdx @@ -65,9 +65,9 @@ re-render when the setter receives a value that `prev_state != next_state`. This hook requires the state object to implement `PartialEq`. ## `use_memo` -`use_memo` is used for obtaining an immutable reference to a value. +`use_memo` is used for obtaining an immutable reference to a memoized value. Its state persists across renders. -Its value will be recalculated if the dependency changes. +Its value will be recalculated only if any of the dependencies values change. `use_memo` can be useful for keeping things in scope for the lifetime of the component, so long as you don't store a clone of the resulting `Rc` anywhere that outlives the component. @@ -75,9 +75,18 @@ you don't store a clone of the resulting `Rc` anywhere that outlives the compone ```rust use yew::{function_component, html, use_memo, use_state, Callback, Html}; +#[derive(PartialEq, Properties)] +pub struct Props { + pub step: usize, +} + #[function_component(UseMemo)] -fn ref_hook() -> Html { - let message = use_memo(|_| "Some Expensive State.".to_string(), ()); +fn ref_hook(props: &Props) -> Html { + // Will only get recalculated if `props.step` value changes + let message = use_memo( + |step: usize| format!("{}. Do Some Expensive Calculation", step), + props.step + ); html! {
diff --git a/website/docs/migration-guides/yew/from-0_19_0-to-0_20_0.mdx b/website/docs/migration-guides/yew/from-0_19_0-to-0_20_0.mdx index a9f464ce2d4..75122adc132 100644 --- a/website/docs/migration-guides/yew/from-0_19_0-to-0_20_0.mdx +++ b/website/docs/migration-guides/yew/from-0_19_0-to-0_20_0.mdx @@ -12,4 +12,6 @@ SSR hydration. They have been removed. Read more in the [github issue](https://g The Function Components and Hooks API are re-implemented with a different mechanism: - User-defined hooks are now required to have a prefix `use_` and must be marked by `#[hook]` attribute. -- Hooks are now only callable under the top level of a function component or a user defined hooks. +- Hooks will now report compile errors if they are not called from the top level of a function component + or a user defined hook. The limitation existed in the previous version of Yew as well. In this version, + It is reported as a compile time error. From 80dc7e2c937f0c89ebed8dbae565ac15fe9b2ed0 Mon Sep 17 00:00:00 2001 From: Kaede Hoshikawa Date: Mon, 24 Jan 2022 22:08:12 +0900 Subject: [PATCH 31/42] Fix website test. --- .../docs/concepts/function-components/pre-defined-hooks.mdx | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/website/docs/concepts/function-components/pre-defined-hooks.mdx b/website/docs/concepts/function-components/pre-defined-hooks.mdx index 043ee9f354a..5402ebfa66d 100644 --- a/website/docs/concepts/function-components/pre-defined-hooks.mdx +++ b/website/docs/concepts/function-components/pre-defined-hooks.mdx @@ -73,7 +73,7 @@ Its value will be recalculated only if any of the dependencies values change. you don't store a clone of the resulting `Rc` anywhere that outlives the component. ```rust -use yew::{function_component, html, use_memo, use_state, Callback, Html}; +use yew::{function_component, html, use_memo, use_state, Callback, Html, Properties}; #[derive(PartialEq, Properties)] pub struct Props { @@ -84,7 +84,7 @@ pub struct Props { fn ref_hook(props: &Props) -> Html { // Will only get recalculated if `props.step` value changes let message = use_memo( - |step: usize| format!("{}. Do Some Expensive Calculation", step), + |step| format!("{}. Do Some Expensive Calculation", step), props.step ); From 347623bd710f6430b3b94c24ec98860811419e63 Mon Sep 17 00:00:00 2001 From: Kaede Hoshikawa Date: Tue, 25 Jan 2022 14:01:13 +0900 Subject: [PATCH 32/42] Reduce implementation complexity. --- packages/yew/src/functional/hooks/mod.rs | 8 ++--- .../yew/src/functional/hooks/use_effect.rs | 29 +++++++++++++------ packages/yew/src/functional/hooks/use_memo.rs | 13 ++++----- .../yew/src/functional/hooks/use_reducer.rs | 14 ++++----- .../yew/src/functional/hooks/use_state.rs | 6 ++-- packages/yew/src/functional/mod.rs | 20 +++---------- 6 files changed, 40 insertions(+), 50 deletions(-) diff --git a/packages/yew/src/functional/hooks/mod.rs b/packages/yew/src/functional/hooks/mod.rs index 1de1b847b84..aea92abb2da 100644 --- a/packages/yew/src/functional/hooks/mod.rs +++ b/packages/yew/src/functional/hooks/mod.rs @@ -60,7 +60,6 @@ impl Hook for BoxedHook<'_, T> { pub(crate) fn use_hook<'hook, T, O>( initializer: impl 'hook + FnOnce() -> T, runner: impl 'hook + FnOnce(&mut T, HookUpdater) -> O, - destructor: impl 'static + FnOnce(&mut T), ) -> impl 'hook + Hook where T: 'static, @@ -69,7 +68,6 @@ where struct HookProvider<'a, T, O> { initializer: Box T + 'a>, runner: Box O + 'a>, - destructor: Box, } impl Hook for HookProvider<'_, T, O> @@ -82,11 +80,10 @@ where let Self { initializer, runner, - destructor, } = self; // Extract current hook - let updater = ctx.next_state(initializer, destructor); + let updater = ctx.next_state(initializer); // Execute the actual hook closure we were given. Let it mutate the hook state and let // it create a callback that takes the mutable hook state. @@ -99,11 +96,10 @@ where HookProvider { initializer: Box::new(initializer), runner: Box::new(runner), - destructor: Box::new(destructor), } } #[hook] pub(crate) fn use_component_scope() -> AnyScope { - use_hook(|| (), |_, updater| updater.scope().clone(), |_| {}) + use_hook(|| (), |_, updater| updater.scope().clone()) } diff --git a/packages/yew/src/functional/hooks/use_effect.rs b/packages/yew/src/functional/hooks/use_effect.rs index 44b1916f48f..87b22fd9464 100644 --- a/packages/yew/src/functional/hooks/use_effect.rs +++ b/packages/yew/src/functional/hooks/use_effect.rs @@ -3,18 +3,34 @@ use std::rc::Rc; type UseEffectRunnerFn = Box D>; -struct UseEffectBase { +struct UseEffectBase +where + D: FnOnce() + 'static, +{ runner_with_deps: Option<(Rc, UseEffectRunnerFn)>, destructor: Option>, deps: Option>, } -#[hook] -fn use_effect_base(callback: impl FnOnce(&T) -> D + 'static, deps: T, effect_changed_fn: R) +impl Drop for UseEffectBase where + D: FnOnce() + 'static, +{ + fn drop(&mut self) { + if let Some(destructor) = self.destructor.take() { + destructor() + } + } +} + +#[hook] +fn use_effect_base( + callback: impl FnOnce(&T) -> D + 'static, + deps: T, + effect_changed_fn: impl FnOnce(Option<&T>, Option<&T>) -> bool + 'static, +) where T: 'static, D: FnOnce() + 'static, - R: FnOnce(Option<&T>, Option<&T>) -> bool + 'static, { let deps = Rc::new(deps); @@ -48,11 +64,6 @@ where false }); }, - |hook| { - if let Some(destructor) = hook.destructor.take() { - destructor() - } - }, ); } diff --git a/packages/yew/src/functional/hooks/use_memo.rs b/packages/yew/src/functional/hooks/use_memo.rs index f37aa4f972d..4e0193e6f80 100644 --- a/packages/yew/src/functional/hooks/use_memo.rs +++ b/packages/yew/src/functional/hooks/use_memo.rs @@ -5,23 +5,23 @@ use std::rc::Rc; /// /// Memoization means it will only get recalculated when provided dependencies update/change #[hook] -pub fn use_memo(memo_fn: impl FnOnce(&Dependents) -> T, deps: Dependents) -> Rc +pub fn use_memo(memo_fn: impl FnOnce(&D) -> T, deps: D) -> Rc where T: 'static, - Dependents: 'static + PartialEq, + D: 'static + PartialEq, { let deps = Rc::new(deps); - pub struct UseMemo + pub struct UseMemo where T: 'static, - Dependents: 'static + PartialEq, + D: 'static + PartialEq, { - inner: Option<(Rc, Rc)>, + inner: Option<(Rc, Rc)>, } use_hook( - || -> UseMemo { UseMemo { inner: None } }, + || -> UseMemo { UseMemo { inner: None } }, move |state, _updater| { state .inner @@ -35,6 +35,5 @@ where val }) }, - |_| {}, ) } diff --git a/packages/yew/src/functional/hooks/use_reducer.rs b/packages/yew/src/functional/hooks/use_reducer.rs index 60d867d77d3..4079aa37451 100644 --- a/packages/yew/src/functional/hooks/use_reducer.rs +++ b/packages/yew/src/functional/hooks/use_reducer.rs @@ -145,11 +145,12 @@ where /// The base function of [`use_reducer`] and [`use_reducer_eq`] #[hook] -fn use_reducer_base(initial_fn: F, should_render_fn: R) -> UseReducerHandle +fn use_reducer_base( + initial_fn: impl FnOnce() -> T, + should_render_fn: impl 'static + Fn(&T, &T) -> bool, +) -> UseReducerHandle where T: Reducible + 'static, - F: FnOnce() -> T, - R: (Fn(&T, &T) -> bool) + 'static, { use_hook( move || UseReducer { @@ -188,7 +189,6 @@ where dispatch, } }, - |_| {}, ) } @@ -261,10 +261,9 @@ where /// } /// ``` #[hook] -pub fn use_reducer(initial_fn: F) -> UseReducerHandle +pub fn use_reducer(initial_fn: impl FnOnce() -> T) -> UseReducerHandle where T: Reducible + 'static, - F: FnOnce() -> T, { use_reducer_base(initial_fn, |_, _| true) } @@ -274,10 +273,9 @@ where /// This requires the state to implement [`PartialEq`] in addition to the [`Reducible`] trait /// required by [`use_reducer`]. #[hook] -pub fn use_reducer_eq(initial_fn: F) -> UseReducerHandle +pub fn use_reducer_eq(initial_fn: impl FnOnce() -> T) -> UseReducerHandle where T: Reducible + PartialEq + 'static, - F: FnOnce() -> T, { use_reducer_base(initial_fn, T::ne) } diff --git a/packages/yew/src/functional/hooks/use_state.rs b/packages/yew/src/functional/hooks/use_state.rs index 5ce1d264138..130ff273cb7 100644 --- a/packages/yew/src/functional/hooks/use_state.rs +++ b/packages/yew/src/functional/hooks/use_state.rs @@ -55,10 +55,9 @@ where /// } /// ``` #[hook] -pub fn use_state(init_fn: F) -> UseStateHandle +pub fn use_state(init_fn: impl FnOnce() -> T) -> UseStateHandle where T: 'static, - F: FnOnce() -> T, { let handle = use_reducer(move || UseStateReducer { value: Rc::new(init_fn()), @@ -71,10 +70,9 @@ where /// /// This hook requires the state to implement [`PartialEq`]. #[hook] -pub fn use_state_eq(init_fn: F) -> UseStateHandle +pub fn use_state_eq(init_fn: impl FnOnce() -> T) -> UseStateHandle where T: PartialEq + 'static, - F: FnOnce() -> T, { let handle = use_reducer_eq(move || UseStateReducer { value: Rc::new(init_fn()), diff --git a/packages/yew/src/functional/mod.rs b/packages/yew/src/functional/mod.rs index 1d88415961f..c27878d6064 100644 --- a/packages/yew/src/functional/mod.rs +++ b/packages/yew/src/functional/mod.rs @@ -17,7 +17,6 @@ use crate::html::{AnyScope, BaseComponent, HtmlResult}; use crate::Properties; use std::cell::{RefCell, RefMut}; use std::fmt; -use std::ops::DerefMut; use std::rc::Rc; mod hooks; @@ -67,7 +66,6 @@ pub struct HookContext { process_message: ProcessMessage, hooks: Vec>>, - destroy_listeners: Vec>, counter: usize, #[cfg(debug_assertions)] @@ -75,11 +73,7 @@ pub struct HookContext { } impl HookContext { - pub(crate) fn next_state( - &mut self, - initializer: impl FnOnce() -> T, - destructor: impl FnOnce(&mut T) + 'static, - ) -> HookUpdater + pub(crate) fn next_state(&mut self, initializer: impl FnOnce() -> T) -> HookUpdater where T: 'static, { @@ -93,12 +87,6 @@ impl HookContext { let initial_state = Rc::new(RefCell::new(initializer())); self.hooks.push(initial_state.clone()); - { - let initial_state = initial_state.clone(); - self.destroy_listeners.push(Box::new(move || { - destructor(initial_state.borrow_mut().deref_mut()); - })); - } initial_state } }; @@ -168,7 +156,6 @@ where }) }, hooks: vec![], - destroy_listeners: vec![], counter: 0, #[cfg(debug_assertions)] @@ -229,8 +216,9 @@ where fn destroy(&mut self, _ctx: &Context) { let mut hook_ctx = self.hook_ctx.borrow_mut(); - for hook in hook_ctx.destroy_listeners.drain(..) { - hook() + + for hook in hook_ctx.hooks.drain(..) { + drop(hook); } } } From e974dfff6f926095fa8ec02b79bbef1acad4d709 Mon Sep 17 00:00:00 2001 From: Kaede Hoshikawa Date: Tue, 25 Jan 2022 19:17:43 +0900 Subject: [PATCH 33/42] Destructor is no more. --- packages/yew/src/functional/hooks/mod.rs | 1 - 1 file changed, 1 deletion(-) diff --git a/packages/yew/src/functional/hooks/mod.rs b/packages/yew/src/functional/hooks/mod.rs index aea92abb2da..76dcf679409 100644 --- a/packages/yew/src/functional/hooks/mod.rs +++ b/packages/yew/src/functional/hooks/mod.rs @@ -56,7 +56,6 @@ impl Hook for BoxedHook<'_, T> { /// /// The `initializer` callback is called once to create the initial state of the hook. /// `runner` callback handles the logic of the hook. It is called when the hook function is called. -/// `destructor`, as the name implies, is called to cleanup the leftovers of the hook. pub(crate) fn use_hook<'hook, T, O>( initializer: impl 'hook + FnOnce() -> T, runner: impl 'hook + FnOnce(&mut T, HookUpdater) -> O, From 7160c18aceb576bcbe9ac542ce87b26b6127b04e Mon Sep 17 00:00:00 2001 From: Kaede Hoshikawa Date: Wed, 26 Jan 2022 19:44:00 +0900 Subject: [PATCH 34/42] Documentation and macros. --- packages/yew-macro/src/hook/body.rs | 7 ++++- .../hook_location-fail.stderr | 28 +++++++++++++++++ .../hook_location-pass.rs | 30 +++++++++++-------- packages/yew/src/functional/hooks/mod.rs | 5 ++-- .../yew/from-0_19_0-to-0_20_0.mdx | 4 +-- 5 files changed, 56 insertions(+), 18 deletions(-) diff --git a/packages/yew-macro/src/hook/body.rs b/packages/yew-macro/src/hook/body.rs index 8cd2d0d3f07..ce24934ed20 100644 --- a/packages/yew-macro/src/hook/body.rs +++ b/packages/yew-macro/src/hook/body.rs @@ -37,7 +37,12 @@ impl VisitMut for BodyRewriter { if let Some(m) = m.path.segments.last().as_ref().map(|m| &m.ident) { if m.to_string().starts_with("use_") { if self.is_branched() { - emit_error!(m, "hooks cannot be called at this position."); + emit_error!( + m, + "hooks cannot be called at this position."; + help = "move hooks to the top-level of your function."; + note = "see: https://yew.rs/docs/next/concepts/function-components/introduction#hooks" + ); } else { *i = parse_quote_spanned! { i.span() => ::yew::functional::Hook::run(#i, #ctx_ident) }; } diff --git a/packages/yew-macro/tests/function_component_attr/hook_location-fail.stderr b/packages/yew-macro/tests/function_component_attr/hook_location-fail.stderr index 2559f7db859..d40b4cfef08 100644 --- a/packages/yew-macro/tests/function_component_attr/hook_location-fail.stderr +++ b/packages/yew-macro/tests/function_component_attr/hook_location-fail.stderr @@ -1,40 +1,68 @@ error: hooks cannot be called at this position. + + = help: move hooks to the top-level of your function. + = note: see: https://yew.rs/docs/next/concepts/function-components/introduction#hooks + --> tests/function_component_attr/hook_location-fail.rs:9:9 | 9 | use_context::().unwrap(); | ^^^^^^^^^^^ error: hooks cannot be called at this position. + + = help: move hooks to the top-level of your function. + = note: see: https://yew.rs/docs/next/concepts/function-components/introduction#hooks + --> tests/function_component_attr/hook_location-fail.rs:14:9 | 14 | use_context::().unwrap(); | ^^^^^^^^^^^ error: hooks cannot be called at this position. + + = help: move hooks to the top-level of your function. + = note: see: https://yew.rs/docs/next/concepts/function-components/introduction#hooks + --> tests/function_component_attr/hook_location-fail.rs:19:9 | 19 | use_context::().unwrap(); | ^^^^^^^^^^^ error: hooks cannot be called at this position. + + = help: move hooks to the top-level of your function. + = note: see: https://yew.rs/docs/next/concepts/function-components/introduction#hooks + --> tests/function_component_attr/hook_location-fail.rs:22:26 | 22 | while let Some(_m) = use_context::() { | ^^^^^^^^^^^ error: hooks cannot be called at this position. + + = help: move hooks to the top-level of your function. + = note: see: https://yew.rs/docs/next/concepts/function-components/introduction#hooks + --> tests/function_component_attr/hook_location-fail.rs:23:9 | 23 | use_context::().unwrap(); | ^^^^^^^^^^^ error: hooks cannot be called at this position. + + = help: move hooks to the top-level of your function. + = note: see: https://yew.rs/docs/next/concepts/function-components/introduction#hooks + --> tests/function_component_attr/hook_location-fail.rs:27:20 | 27 | Some(_) => use_context::(), | ^^^^^^^^^^^ error: hooks cannot be called at this position. + + = help: move hooks to the top-level of your function. + = note: see: https://yew.rs/docs/next/concepts/function-components/introduction#hooks + --> tests/function_component_attr/hook_location-fail.rs:34:9 | 34 | use_context::().unwrap(); diff --git a/packages/yew-macro/tests/function_component_attr/hook_location-pass.rs b/packages/yew-macro/tests/function_component_attr/hook_location-pass.rs index 3e95f08c546..b2bcbe111fb 100644 --- a/packages/yew-macro/tests/function_component_attr/hook_location-pass.rs +++ b/packages/yew-macro/tests/function_component_attr/hook_location-pass.rs @@ -1,24 +1,28 @@ -use yew::prelude::*; +#![no_implicit_prelude] -#[derive(Debug, PartialEq, Clone)] +#[derive( + ::std::prelude::rust_2021::Debug, + ::std::prelude::rust_2021::PartialEq, + ::std::prelude::rust_2021::Clone, +)] struct Ctx; -#[function_component] -fn Comp() -> Html { - use_context::().unwrap(); +#[::yew::prelude::function_component] +fn Comp() -> ::yew::prelude::Html { + ::yew::prelude::use_context::().unwrap(); - if let Some(_m) = use_context::() { - todo!() + if let ::std::prelude::rust_2021::Some(_m) = ::yew::prelude::use_context::() { + ::std::todo!() } - let _ctx = { use_context::() }; + let _ctx = { ::yew::prelude::use_context::() }; - match use_context::() { - Some(_) => { - todo!() + match ::yew::prelude::use_context::() { + ::std::prelude::rust_2021::Some(_) => { + ::std::todo!() } - None => { - todo!() + ::std::prelude::rust_2021::None => { + ::std::todo!() } } } diff --git a/packages/yew/src/functional/hooks/mod.rs b/packages/yew/src/functional/hooks/mod.rs index 76dcf679409..68632fe954d 100644 --- a/packages/yew/src/functional/hooks/mod.rs +++ b/packages/yew/src/functional/hooks/mod.rs @@ -16,8 +16,9 @@ use crate::functional::{hook, AnyScope, HookContext, HookUpdater}; /// A trait that is implemented on hooks. /// -/// A hook is usually defined via `#[hooks]`. Please refer to its documentation on how to implement -/// hooks. +/// Hooks are defined via the [`#[hooks]`] macro. It provides rewrites to hook invocations +/// and ensures that hooks can only be called at the top-level of a function component or a hook. +/// Please refer to its documentation on how to implement hooks. pub trait Hook { /// The return type when a hook is run. type Output; diff --git a/website/docs/migration-guides/yew/from-0_19_0-to-0_20_0.mdx b/website/docs/migration-guides/yew/from-0_19_0-to-0_20_0.mdx index 75122adc132..abccf2959b7 100644 --- a/website/docs/migration-guides/yew/from-0_19_0-to-0_20_0.mdx +++ b/website/docs/migration-guides/yew/from-0_19_0-to-0_20_0.mdx @@ -7,11 +7,11 @@ title: "From 0.19.0 to 0.20.0" This method of controlling body has caused issues in event registration and SSR hydration. They have been removed. Read more in the [github issue](https://github.com/yewstack/yew/pull/2346). -## Hooks and Function Components V2 +## New Hooks and Function Components API The Function Components and Hooks API are re-implemented with a different mechanism: -- User-defined hooks are now required to have a prefix `use_` and must be marked by `#[hook]` attribute. +- User-defined hooks are now required to have a prefix `use_` and must be marked with the `#[hook]` attribute. - Hooks will now report compile errors if they are not called from the top level of a function component or a user defined hook. The limitation existed in the previous version of Yew as well. In this version, It is reported as a compile time error. From aa2e8034dbb70efac6bdb39292abb59d1d0e793a Mon Sep 17 00:00:00 2001 From: Kaede Hoshikawa Date: Thu, 27 Jan 2022 20:06:13 +0900 Subject: [PATCH 35/42] Reduce heap allocation and hook complexity. --- packages/yew/src/functional/hooks/mod.rs | 55 +----- .../yew/src/functional/hooks/use_effect.rs | 129 ++++++++----- packages/yew/src/functional/hooks/use_memo.rs | 43 ++--- .../yew/src/functional/hooks/use_reducer.rs | 107 ++++++----- packages/yew/src/functional/mod.rs | 171 +++++++----------- 5 files changed, 249 insertions(+), 256 deletions(-) diff --git a/packages/yew/src/functional/hooks/mod.rs b/packages/yew/src/functional/hooks/mod.rs index 68632fe954d..776f66eca29 100644 --- a/packages/yew/src/functional/hooks/mod.rs +++ b/packages/yew/src/functional/hooks/mod.rs @@ -12,7 +12,7 @@ pub use use_reducer::*; pub use use_ref::*; pub use use_state::*; -use crate::functional::{hook, AnyScope, HookContext, HookUpdater}; +use crate::functional::{AnyScope, HookContext}; /// A trait that is implemented on hooks. /// @@ -49,57 +49,16 @@ impl Hook for BoxedHook<'_, T> { } } -/// Low level building block of creating hooks. -/// -/// It is used to created the pre-defined primitive hooks. -/// Generally, it isn't needed to create hooks and should be avoided as most custom hooks can be -/// created by combining other hooks as described in [Yew Docs]. -/// -/// The `initializer` callback is called once to create the initial state of the hook. -/// `runner` callback handles the logic of the hook. It is called when the hook function is called. -pub(crate) fn use_hook<'hook, T, O>( - initializer: impl 'hook + FnOnce() -> T, - runner: impl 'hook + FnOnce(&mut T, HookUpdater) -> O, -) -> impl 'hook + Hook -where - T: 'static, - O: 'hook, -{ - struct HookProvider<'a, T, O> { - initializer: Box T + 'a>, - runner: Box O + 'a>, - } +pub(crate) fn use_component_scope() -> impl Hook { + struct HookProvider {} - impl Hook for HookProvider<'_, T, O> - where - T: 'static, - { - type Output = O; + impl Hook for HookProvider { + type Output = AnyScope; fn run(self, ctx: &mut HookContext) -> Self::Output { - let Self { - initializer, - runner, - } = self; - - // Extract current hook - let updater = ctx.next_state(initializer); - - // Execute the actual hook closure we were given. Let it mutate the hook state and let - // it create a callback that takes the mutable hook state. - let mut hook = updater.borrow_mut::(); - - runner(&mut *hook, updater.clone()) + ctx.scope.clone() } } - HookProvider { - initializer: Box::new(initializer), - runner: Box::new(runner), - } -} - -#[hook] -pub(crate) fn use_component_scope() -> AnyScope { - use_hook(|| (), |_, updater| updater.scope().clone()) + HookProvider {} } diff --git a/packages/yew/src/functional/hooks/use_effect.rs b/packages/yew/src/functional/hooks/use_effect.rs index 87b22fd9464..f3541340854 100644 --- a/packages/yew/src/functional/hooks/use_effect.rs +++ b/packages/yew/src/functional/hooks/use_effect.rs @@ -1,20 +1,54 @@ -use crate::functional::{hook, use_hook}; +use std::cell::RefCell; use std::rc::Rc; -type UseEffectRunnerFn = Box D>; +use crate::functional::{hook, Effect, Hook, HookContext}; -struct UseEffectBase +struct UseEffectBase where + C: FnOnce(&T) -> D + 'static, + T: 'static, D: FnOnce() + 'static, + R: Fn(Option<&T>, Option<&T>) -> bool + 'static, { - runner_with_deps: Option<(Rc, UseEffectRunnerFn)>, - destructor: Option>, + runner_with_deps: Option<(Rc, C)>, + destructor: Option, deps: Option>, + effect_changed_fn: R, +} + +impl Effect for RefCell> +where + C: FnOnce(&T) -> D + 'static, + T: 'static, + D: FnOnce() + 'static, + R: Fn(Option<&T>, Option<&T>) -> bool + 'static, +{ + fn rendered(&self) { + let mut this = self.borrow_mut(); + + if let Some((deps, callback)) = this.runner_with_deps.take() { + if !(this.effect_changed_fn)(Some(&*deps), this.deps.as_deref()) { + return; + } + + if let Some(de) = this.destructor.take() { + de(); + } + + let new_destructor = callback(&deps); + + this.deps = Some(deps); + this.destructor = Some(new_destructor); + } + } } -impl Drop for UseEffectBase +impl Drop for UseEffectBase where + C: FnOnce(&T) -> D + 'static, + T: 'static, D: FnOnce() + 'static, + R: Fn(Option<&T>, Option<&T>) -> bool + 'static, { fn drop(&mut self) { if let Some(destructor) = self.destructor.take() { @@ -23,48 +57,61 @@ where } } -#[hook] fn use_effect_base( callback: impl FnOnce(&T) -> D + 'static, deps: T, - effect_changed_fn: impl FnOnce(Option<&T>, Option<&T>) -> bool + 'static, -) where + effect_changed_fn: impl Fn(Option<&T>, Option<&T>) -> bool + 'static, +) -> impl Hook +where T: 'static, D: FnOnce() + 'static, { - let deps = Rc::new(deps); - - use_hook( - move || { - let destructor: Option> = None; - UseEffectBase { - runner_with_deps: None, - destructor, - deps: None, - } - }, - move |state, updater| { - state.runner_with_deps = Some((deps, Box::new(callback))); - - updater.post_render(move |state: &mut UseEffectBase| { - if let Some((deps, callback)) = state.runner_with_deps.take() { - if !effect_changed_fn(Some(&*deps), state.deps.as_deref()) { - return false; - } - - if let Some(de) = state.destructor.take() { - de(); - } - - let new_destructor = callback(&deps); - - state.deps = Some(deps); - state.destructor = Some(Box::new(new_destructor)); - } - false + struct HookProvider + where + C: FnOnce(&T) -> D + 'static, + T: 'static, + D: FnOnce() + 'static, + R: Fn(Option<&T>, Option<&T>) -> bool + 'static, + { + callback: C, + deps: Rc, + effect_changed_fn: R, + } + + impl Hook for HookProvider + where + C: FnOnce(&T) -> D + 'static, + T: 'static, + D: FnOnce() + 'static, + R: Fn(Option<&T>, Option<&T>) -> bool + 'static, + { + type Output = (); + + fn run(self, ctx: &mut HookContext) -> Self::Output { + let Self { + callback, + deps, + effect_changed_fn, + } = self; + + let state = ctx.next_effect(|_| -> RefCell> { + RefCell::new(UseEffectBase { + runner_with_deps: None, + destructor: None, + deps: None, + effect_changed_fn, + }) }); - }, - ); + + state.borrow_mut().runner_with_deps = Some((deps, callback)); + } + } + + HookProvider { + callback, + deps: Rc::new(deps), + effect_changed_fn, + } } /// This hook is used for hooking into the component's lifecycle. diff --git a/packages/yew/src/functional/hooks/use_memo.rs b/packages/yew/src/functional/hooks/use_memo.rs index 4e0193e6f80..174151f2e41 100644 --- a/packages/yew/src/functional/hooks/use_memo.rs +++ b/packages/yew/src/functional/hooks/use_memo.rs @@ -1,6 +1,8 @@ -use crate::functional::{hook, use_hook}; +use std::cell::RefCell; use std::rc::Rc; +use crate::functional::{hook, use_state}; + /// Get a immutable reference to a memoized value /// /// Memoization means it will only get recalculated when provided dependencies update/change @@ -10,30 +12,25 @@ where T: 'static, D: 'static + PartialEq, { - let deps = Rc::new(deps); + let val = use_state(|| -> RefCell>> { RefCell::new(None) }); + let last_deps = use_state(|| -> RefCell> { RefCell::new(None) }); - pub struct UseMemo - where - T: 'static, - D: 'static + PartialEq, - { - inner: Option<(Rc, Rc)>, - } + let mut val = val.borrow_mut(); + let mut last_deps = last_deps.borrow_mut(); - use_hook( - || -> UseMemo { UseMemo { inner: None } }, - move |state, _updater| { - state - .inner - .as_ref() - .and_then(|(m, n)| (m.as_ref() == &*deps).then(|| n.clone())) - .unwrap_or_else(|| { - let val = Rc::new(memo_fn(&deps)); + match ( + val.as_ref(), + last_deps.as_ref().and_then(|m| (m != &deps).then(|| ())), + ) { + // Previous value exists and last_deps == deps + (Some(m), None) => m.clone(), + _ => { + let new_val = Rc::new(memo_fn(&deps)); + *last_deps = Some(deps); - state.inner = Some((deps, val.clone())); + *val = Some(new_val.clone()); - val - }) - }, - ) + new_val + } + } } diff --git a/packages/yew/src/functional/hooks/use_reducer.rs b/packages/yew/src/functional/hooks/use_reducer.rs index 4079aa37451..e7bcad33054 100644 --- a/packages/yew/src/functional/hooks/use_reducer.rs +++ b/packages/yew/src/functional/hooks/use_reducer.rs @@ -1,9 +1,10 @@ use std::cell::RefCell; use std::fmt; +use std::marker::PhantomData; use std::ops::Deref; use std::rc::Rc; -use crate::functional::{hook, use_hook}; +use crate::functional::{hook, Hook, HookContext}; type DispatchFn = Rc::Action)>; @@ -20,10 +21,10 @@ struct UseReducer where T: Reducible, { - current_state: Rc, + current_state: Rc>>, // To be replaced with OnceCell once it becomes available in std. - dispatch: RefCell>>, + dispatch: DispatchFn, } /// State handle for [`use_reducer`] and [`use_reducer_eq`] hook @@ -144,52 +145,76 @@ where } /// The base function of [`use_reducer`] and [`use_reducer_eq`] -#[hook] -fn use_reducer_base( - initial_fn: impl FnOnce() -> T, +fn use_reducer_base<'hook, T>( + initial_fn: impl 'hook + FnOnce() -> T, should_render_fn: impl 'static + Fn(&T, &T) -> bool, -) -> UseReducerHandle +) -> impl 'hook + Hook> where T: Reducible + 'static, { - use_hook( - move || UseReducer { - current_state: Rc::new(initial_fn()), - dispatch: RefCell::default(), - }, - |s, updater| { - let mut dispatch_ref = s.dispatch.borrow_mut(); - - // Create dispatch once. - let dispatch = match *dispatch_ref { - Some(ref m) => (*m).to_owned(), - None => { - let should_render_fn = Rc::new(should_render_fn); - - let dispatch: Rc = Rc::new(move |action: T::Action| { - let should_render_fn = should_render_fn.clone(); - - updater.callback(move |state: &mut UseReducer| { - let next_state = state.current_state.clone().reduce(action); - let should_render = should_render_fn(&next_state, &state.current_state); - state.current_state = next_state; + struct HookProvider<'hook, T, F, R> + where + T: Reducible + 'static, + F: 'hook + FnOnce() -> T, + R: 'static + Fn(&T, &T) -> bool, + { + _marker: PhantomData<&'hook ()>, - should_render - }); - }); + initial_fn: F, + should_render_fn: R, + } - *dispatch_ref = Some(dispatch.clone()); + impl<'hook, T, F, R> Hook for HookProvider<'hook, T, F, R> + where + T: Reducible + 'static, + F: 'hook + FnOnce() -> T, + R: 'static + Fn(&T, &T) -> bool, + { + type Output = UseReducerHandle; - dispatch + fn run(self, ctx: &mut HookContext) -> Self::Output { + let Self { + initial_fn, + should_render_fn, + .. + } = self; + + let state = ctx.next_state(move |re_render| { + let val = Rc::new(RefCell::new(Rc::new(initial_fn()))); + let should_render_fn = Rc::new(should_render_fn); + + UseReducer { + current_state: val.clone(), + dispatch: Rc::new(move |action: T::Action| { + let should_render = { + let should_render_fn = should_render_fn.clone(); + let mut val = val.borrow_mut(); + let next_val = (*val).clone().reduce(action); + let should_render = should_render_fn(&next_val, &val); + *val = next_val; + + should_render + }; + + if should_render { + re_render() + } + }), } - }; - - UseReducerHandle { - value: Rc::clone(&s.current_state), - dispatch, - } - }, - ) + }); + + let value = state.current_state.borrow().clone(); + let dispatch = state.dispatch.clone(); + + UseReducerHandle { value, dispatch } + } + } + + HookProvider { + _marker: PhantomData, + initial_fn, + should_render_fn, + } } /// This hook is an alternative to [`use_state`](super::use_state()). diff --git a/packages/yew/src/functional/mod.rs b/packages/yew/src/functional/mod.rs index c27878d6064..bde52d53288 100644 --- a/packages/yew/src/functional/mod.rs +++ b/packages/yew/src/functional/mod.rs @@ -15,7 +15,8 @@ use crate::html::{AnyScope, BaseComponent, HtmlResult}; use crate::Properties; -use std::cell::{RefCell, RefMut}; +use std::any::Any; +use std::cell::RefCell; use std::fmt; use std::rc::Rc; @@ -57,15 +58,40 @@ pub use yew_macro::function_component; /// This attribute creates a user-defined hook from a normal Rust function. pub use yew_macro::hook; -type Msg = Box bool>; -type ProcessMessage = Rc; +type ReRender = Rc; + +#[derive(Clone)] +struct Queue(Rc>>); + +impl Default for Queue { + fn default() -> Self { + Queue(Rc::default()) + } +} + +impl Queue { + fn push(&self, msg: T) { + self.0.borrow_mut().push(msg); + } + + fn drain(&self) -> Vec { + self.0.borrow_mut().drain(..).collect() + } +} + +/// Primitives of a Hook state. +pub(crate) trait Effect { + fn rendered(&self) {} +} /// A hook context to be passed to hooks. pub struct HookContext { - scope: AnyScope, + pub(crate) scope: AnyScope, + + re_render: ReRender, + states: Vec>, - process_message: ProcessMessage, - hooks: Vec>>, + effects: Queue>, counter: usize, #[cfg(debug_assertions)] @@ -73,7 +99,7 @@ pub struct HookContext { } impl HookContext { - pub(crate) fn next_state(&mut self, initializer: impl FnOnce() -> T) -> HookUpdater + pub(crate) fn next_state(&mut self, initializer: impl FnOnce(ReRender) -> T) -> Rc where T: 'static, { @@ -81,21 +107,32 @@ impl HookContext { let hook_pos = self.counter; self.counter += 1; - let hook = match self.hooks.get(hook_pos).cloned() { + let state = match self.states.get(hook_pos).cloned() { Some(m) => m, None => { - let initial_state = Rc::new(RefCell::new(initializer())); - self.hooks.push(initial_state.clone()); + let initial_state = Rc::new(initializer(self.re_render.clone())); + self.states.push(initial_state.clone()); initial_state } }; - HookUpdater { - hook, - scope: self.scope.clone(), - process_message: self.process_message.clone(), + state.downcast().unwrap() + } + + pub(crate) fn next_effect(&mut self, initializer: impl FnOnce(ReRender) -> T) -> Rc + where + T: 'static + Effect, + { + let last_counter = self.counter; + let t = self.next_state(initializer); + + // This is a new effect, we push it into the effects queue. + if self.counter != last_counter { + self.effects.push(t.clone()); } + + t } } @@ -120,7 +157,7 @@ pub trait FunctionProvider { pub struct FunctionComponent { _never: std::marker::PhantomData, hook_ctx: RefCell, - message_queue: MsgQueue, + effects: Queue>, } impl fmt::Debug for FunctionComponent { @@ -133,29 +170,24 @@ impl BaseComponent for FunctionComponent where T: FunctionProvider + 'static, { - type Message = Box bool>; + type Message = (); type Properties = T::TProps; fn create(ctx: &Context) -> Self { let scope = AnyScope::from(ctx.link().clone()); - let message_queue = MsgQueue::default(); + let effects = Queue::default(); Self { _never: std::marker::PhantomData::default(), - message_queue: message_queue.clone(), + effects: effects.clone(), hook_ctx: RefCell::new(HookContext { + effects, scope, - process_message: { - let scope = ctx.link().clone(); - Rc::new(move |msg, post_render| { - if post_render { - message_queue.push(msg); - } else { - scope.send_message(msg); - } - }) + re_render: { + let link = ctx.link().clone(); + Rc::new(move || link.send_message(())) }, - hooks: vec![], + states: vec![], counter: 0, #[cfg(debug_assertions)] @@ -164,8 +196,8 @@ where } } - fn update(&mut self, _ctx: &Context, msg: Self::Message) -> bool { - msg() + fn update(&mut self, _ctx: &Context, _msg: Self::Message) -> bool { + true } fn changed(&mut self, _ctx: &Context) -> bool { @@ -208,87 +240,20 @@ where result } - fn rendered(&mut self, ctx: &Context, _first_render: bool) { - for msg in self.message_queue.drain() { - ctx.link().send_message(msg); + fn rendered(&mut self, _ctx: &Context, _first_render: bool) { + for effect in self.effects.0.borrow().iter() { + effect.rendered(); } } fn destroy(&mut self, _ctx: &Context) { + self.effects.drain(); let mut hook_ctx = self.hook_ctx.borrow_mut(); - for hook in hook_ctx.hooks.drain(..) { - drop(hook); + for state in hook_ctx.states.drain(..) { + drop(state); } } } impl SealedBaseComponent for FunctionComponent where T: FunctionProvider + 'static {} - -#[derive(Clone, Default)] -struct MsgQueue(Rc>>); - -impl MsgQueue { - fn push(&self, msg: Msg) { - self.0.borrow_mut().push(msg); - } - - fn drain(&self) -> Vec { - self.0.borrow_mut().drain(..).collect() - } -} - -/// The `HookUpdater` provides a convenient interface for hooking into the lifecycle of -/// the underlying Yew Component that backs the function component. -#[derive(Clone)] -pub(crate) struct HookUpdater { - hook: Rc>, - scope: AnyScope, - process_message: ProcessMessage, -} - -impl HookUpdater { - /// Retrieves the hook state - pub fn borrow_mut(&self) -> RefMut<'_, T> { - RefMut::map(self.hook.borrow_mut(), |m| { - m.downcast_mut() - .expect("incompatible hook type. Hooks must always be called in the same order") - }) - } - - /// Registers a callback to be run immediately. - pub fn callback(&self, cb: impl FnOnce(&mut T) -> bool + 'static) { - let this = self.clone(); - - // Update the component - // We're calling "link.send_message", so we're not calling it post-render - let post_render = false; - (self.process_message)( - Box::new(move || { - let mut hook = this.borrow_mut(); - cb(&mut *hook) - }), - post_render, - ); - } - - /// Registers a callback to be run after the render - pub fn post_render(&self, cb: impl FnOnce(&mut T) -> bool + 'static) { - let this = self.clone(); - - // Update the component - // We're calling "message_queue.push", so not calling it post-render - let post_render = true; - (self.process_message)( - Box::new(move || { - let mut hook = this.borrow_mut(); - cb(&mut *hook) - }), - post_render, - ); - } - - pub fn scope(&self) -> &AnyScope { - &self.scope - } -} From 179910277c10c9eeb2059126f2d4e98452fbb6de Mon Sep 17 00:00:00 2001 From: Kaede Hoshikawa Date: Thu, 27 Jan 2022 20:21:33 +0900 Subject: [PATCH 36/42] Remove Queue as well. --- .../yew/src/functional/hooks/use_effect.rs | 26 +++++------ packages/yew/src/functional/mod.rs | 44 +++++-------------- 2 files changed, 25 insertions(+), 45 deletions(-) diff --git a/packages/yew/src/functional/hooks/use_effect.rs b/packages/yew/src/functional/hooks/use_effect.rs index f3541340854..f43fe3da653 100644 --- a/packages/yew/src/functional/hooks/use_effect.rs +++ b/packages/yew/src/functional/hooks/use_effect.rs @@ -3,22 +3,22 @@ use std::rc::Rc; use crate::functional::{hook, Effect, Hook, HookContext}; -struct UseEffectBase +struct UseEffectBase where - C: FnOnce(&T) -> D + 'static, + F: FnOnce(&T) -> D + 'static, T: 'static, D: FnOnce() + 'static, R: Fn(Option<&T>, Option<&T>) -> bool + 'static, { - runner_with_deps: Option<(Rc, C)>, + runner_with_deps: Option<(Rc, F)>, destructor: Option, deps: Option>, effect_changed_fn: R, } -impl Effect for RefCell> +impl Effect for RefCell> where - C: FnOnce(&T) -> D + 'static, + F: FnOnce(&T) -> D + 'static, T: 'static, D: FnOnce() + 'static, R: Fn(Option<&T>, Option<&T>) -> bool + 'static, @@ -43,9 +43,9 @@ where } } -impl Drop for UseEffectBase +impl Drop for UseEffectBase where - C: FnOnce(&T) -> D + 'static, + F: FnOnce(&T) -> D + 'static, T: 'static, D: FnOnce() + 'static, R: Fn(Option<&T>, Option<&T>) -> bool + 'static, @@ -66,21 +66,21 @@ where T: 'static, D: FnOnce() + 'static, { - struct HookProvider + struct HookProvider where - C: FnOnce(&T) -> D + 'static, + F: FnOnce(&T) -> D + 'static, T: 'static, D: FnOnce() + 'static, R: Fn(Option<&T>, Option<&T>) -> bool + 'static, { - callback: C, + callback: F, deps: Rc, effect_changed_fn: R, } - impl Hook for HookProvider + impl Hook for HookProvider where - C: FnOnce(&T) -> D + 'static, + F: FnOnce(&T) -> D + 'static, T: 'static, D: FnOnce() + 'static, R: Fn(Option<&T>, Option<&T>) -> bool + 'static, @@ -94,7 +94,7 @@ where effect_changed_fn, } = self; - let state = ctx.next_effect(|_| -> RefCell> { + let state = ctx.next_effect(|_| -> RefCell> { RefCell::new(UseEffectBase { runner_with_deps: None, destructor: None, diff --git a/packages/yew/src/functional/mod.rs b/packages/yew/src/functional/mod.rs index bde52d53288..4d3f587ec36 100644 --- a/packages/yew/src/functional/mod.rs +++ b/packages/yew/src/functional/mod.rs @@ -60,25 +60,6 @@ pub use yew_macro::hook; type ReRender = Rc; -#[derive(Clone)] -struct Queue(Rc>>); - -impl Default for Queue { - fn default() -> Self { - Queue(Rc::default()) - } -} - -impl Queue { - fn push(&self, msg: T) { - self.0.borrow_mut().push(msg); - } - - fn drain(&self) -> Vec { - self.0.borrow_mut().drain(..).collect() - } -} - /// Primitives of a Hook state. pub(crate) trait Effect { fn rendered(&self) {} @@ -87,11 +68,10 @@ pub(crate) trait Effect { /// A hook context to be passed to hooks. pub struct HookContext { pub(crate) scope: AnyScope, - re_render: ReRender, - states: Vec>, - effects: Queue>, + states: Vec>, + effects: Vec>, counter: usize, #[cfg(debug_assertions)] @@ -124,11 +104,11 @@ impl HookContext { where T: 'static + Effect, { - let last_counter = self.counter; + let prev_state_len = self.states.len(); let t = self.next_state(initializer); - // This is a new effect, we push it into the effects queue. - if self.counter != last_counter { + // This is a new effect, we add it to effects. + if self.states.len() != prev_state_len { self.effects.push(t.clone()); } @@ -157,7 +137,6 @@ pub trait FunctionProvider { pub struct FunctionComponent { _never: std::marker::PhantomData, hook_ctx: RefCell, - effects: Queue>, } impl fmt::Debug for FunctionComponent { @@ -175,19 +154,17 @@ where fn create(ctx: &Context) -> Self { let scope = AnyScope::from(ctx.link().clone()); - let effects = Queue::default(); Self { _never: std::marker::PhantomData::default(), - effects: effects.clone(), hook_ctx: RefCell::new(HookContext { - effects, + effects: Vec::new(), scope, re_render: { let link = ctx.link().clone(); Rc::new(move || link.send_message(())) }, - states: vec![], + states: Vec::new(), counter: 0, #[cfg(debug_assertions)] @@ -241,14 +218,17 @@ where } fn rendered(&mut self, _ctx: &Context, _first_render: bool) { - for effect in self.effects.0.borrow().iter() { + let hook_ctx = self.hook_ctx.borrow(); + + for effect in hook_ctx.effects.iter() { effect.rendered(); } } fn destroy(&mut self, _ctx: &Context) { - self.effects.drain(); let mut hook_ctx = self.hook_ctx.borrow_mut(); + // We clear the effects as these are also references to states. + hook_ctx.effects.clear(); for state in hook_ctx.states.drain(..) { drop(state); From acc87d103e032cfd77133e8fcdb2d74ff92f7337 Mon Sep 17 00:00:00 2001 From: Kaede Hoshikawa Date: Thu, 27 Jan 2022 20:39:22 +0900 Subject: [PATCH 37/42] Prefer Generics. --- .../yew/src/functional/hooks/use_context.rs | 4 ++-- .../yew/src/functional/hooks/use_effect.rs | 20 ++++++++++--------- packages/yew/src/functional/hooks/use_memo.rs | 5 +++-- .../yew/src/functional/hooks/use_reducer.rs | 20 ++++++++++--------- packages/yew/src/functional/hooks/use_ref.rs | 11 ++++++---- .../yew/src/functional/hooks/use_state.rs | 6 ++++-- 6 files changed, 38 insertions(+), 28 deletions(-) diff --git a/packages/yew/src/functional/hooks/use_context.rs b/packages/yew/src/functional/hooks/use_context.rs index 0a2489c9761..24fb8fcda31 100644 --- a/packages/yew/src/functional/hooks/use_context.rs +++ b/packages/yew/src/functional/hooks/use_context.rs @@ -31,7 +31,7 @@ use crate::functional::{hook, use_component_scope, use_memo, use_state}; /// ``` #[hook] pub fn use_context() -> Option { - struct State { + struct UseContext { context: Option<(T, ContextHandle)>, } @@ -41,7 +41,7 @@ pub fn use_context() -> Option { let state = { let val_dispatcher = val.setter(); use_memo( - move |_| State { + move |_| UseContext { context: scope.context::(Callback::from(move |m| { val_dispatcher.clone().set(Some(m)); })), diff --git a/packages/yew/src/functional/hooks/use_effect.rs b/packages/yew/src/functional/hooks/use_effect.rs index f43fe3da653..cade83f590a 100644 --- a/packages/yew/src/functional/hooks/use_effect.rs +++ b/packages/yew/src/functional/hooks/use_effect.rs @@ -58,7 +58,7 @@ where } fn use_effect_base( - callback: impl FnOnce(&T) -> D + 'static, + runner: impl FnOnce(&T) -> D + 'static, deps: T, effect_changed_fn: impl Fn(Option<&T>, Option<&T>) -> bool + 'static, ) -> impl Hook @@ -73,7 +73,7 @@ where D: FnOnce() + 'static, R: Fn(Option<&T>, Option<&T>) -> bool + 'static, { - callback: F, + runner: F, deps: Rc, effect_changed_fn: R, } @@ -89,7 +89,7 @@ where fn run(self, ctx: &mut HookContext) -> Self::Output { let Self { - callback, + runner, deps, effect_changed_fn, } = self; @@ -103,12 +103,12 @@ where }) }); - state.borrow_mut().runner_with_deps = Some((deps, callback)); + state.borrow_mut().runner_with_deps = Some((deps, runner)); } } HookProvider { - callback, + runner, deps: Rc::new(deps), effect_changed_fn, } @@ -145,11 +145,12 @@ where /// } /// ``` #[hook] -pub fn use_effect(callback: impl FnOnce() -> D + 'static) +pub fn use_effect(f: F) where + F: FnOnce() -> D + 'static, D: FnOnce() + 'static, { - use_effect_base(|_| callback(), (), |_, _| true); + use_effect_base(|_| f(), (), |_, _| true); } /// This hook is similar to [`use_effect`] but it accepts dependencies. @@ -158,10 +159,11 @@ where /// To detect changes, dependencies must implement `PartialEq`. /// Note that the destructor also runs when dependencies change. #[hook] -pub fn use_effect_with_deps(callback: impl FnOnce(&T) -> D + 'static, deps: T) +pub fn use_effect_with_deps(f: F, deps: T) where T: PartialEq + 'static, + F: FnOnce(&T) -> D + 'static, D: FnOnce() + 'static, { - use_effect_base(callback, deps, |lhs, rhs| lhs != rhs) + use_effect_base(f, deps, |lhs, rhs| lhs != rhs) } diff --git a/packages/yew/src/functional/hooks/use_memo.rs b/packages/yew/src/functional/hooks/use_memo.rs index 174151f2e41..f7538c23e64 100644 --- a/packages/yew/src/functional/hooks/use_memo.rs +++ b/packages/yew/src/functional/hooks/use_memo.rs @@ -7,9 +7,10 @@ use crate::functional::{hook, use_state}; /// /// Memoization means it will only get recalculated when provided dependencies update/change #[hook] -pub fn use_memo(memo_fn: impl FnOnce(&D) -> T, deps: D) -> Rc +pub fn use_memo(f: F, deps: D) -> Rc where T: 'static, + F: FnOnce(&D) -> T, D: 'static + PartialEq, { let val = use_state(|| -> RefCell>> { RefCell::new(None) }); @@ -25,7 +26,7 @@ where // Previous value exists and last_deps == deps (Some(m), None) => m.clone(), _ => { - let new_val = Rc::new(memo_fn(&deps)); + let new_val = Rc::new(f(&deps)); *last_deps = Some(deps); *val = Some(new_val.clone()); diff --git a/packages/yew/src/functional/hooks/use_reducer.rs b/packages/yew/src/functional/hooks/use_reducer.rs index e7bcad33054..63c57943ba1 100644 --- a/packages/yew/src/functional/hooks/use_reducer.rs +++ b/packages/yew/src/functional/hooks/use_reducer.rs @@ -146,7 +146,7 @@ where /// The base function of [`use_reducer`] and [`use_reducer_eq`] fn use_reducer_base<'hook, T>( - initial_fn: impl 'hook + FnOnce() -> T, + init_fn: impl 'hook + FnOnce() -> T, should_render_fn: impl 'static + Fn(&T, &T) -> bool, ) -> impl 'hook + Hook> where @@ -160,7 +160,7 @@ where { _marker: PhantomData<&'hook ()>, - initial_fn: F, + init_fn: F, should_render_fn: R, } @@ -174,13 +174,13 @@ where fn run(self, ctx: &mut HookContext) -> Self::Output { let Self { - initial_fn, + init_fn, should_render_fn, .. } = self; let state = ctx.next_state(move |re_render| { - let val = Rc::new(RefCell::new(Rc::new(initial_fn()))); + let val = Rc::new(RefCell::new(Rc::new(init_fn()))); let should_render_fn = Rc::new(should_render_fn); UseReducer { @@ -212,7 +212,7 @@ where HookProvider { _marker: PhantomData, - initial_fn, + init_fn, should_render_fn, } } @@ -286,11 +286,12 @@ where /// } /// ``` #[hook] -pub fn use_reducer(initial_fn: impl FnOnce() -> T) -> UseReducerHandle +pub fn use_reducer(init_fn: F) -> UseReducerHandle where T: Reducible + 'static, + F: FnOnce() -> T, { - use_reducer_base(initial_fn, |_, _| true) + use_reducer_base(init_fn, |_, _| true) } /// [`use_reducer`] but only re-renders when `prev_state != next_state`. @@ -298,9 +299,10 @@ where /// This requires the state to implement [`PartialEq`] in addition to the [`Reducible`] trait /// required by [`use_reducer`]. #[hook] -pub fn use_reducer_eq(initial_fn: impl FnOnce() -> T) -> UseReducerHandle +pub fn use_reducer_eq(init_fn: F) -> UseReducerHandle where T: Reducible + PartialEq + 'static, + F: FnOnce() -> T, { - use_reducer_base(initial_fn, T::ne) + use_reducer_base(init_fn, T::ne) } diff --git a/packages/yew/src/functional/hooks/use_ref.rs b/packages/yew/src/functional/hooks/use_ref.rs index 7fc847a7fa8..81cc83a249f 100644 --- a/packages/yew/src/functional/hooks/use_ref.rs +++ b/packages/yew/src/functional/hooks/use_ref.rs @@ -1,7 +1,7 @@ use std::cell::RefCell; use std::rc::Rc; -use crate::functional::{hook, use_memo}; +use crate::functional::{hook, use_memo, use_state}; use crate::NodeRef; /// This hook is used for obtaining a mutable reference to a stateful value. @@ -51,8 +51,11 @@ use crate::NodeRef; /// } /// ``` #[hook] -pub fn use_mut_ref(initial_value: impl FnOnce() -> T) -> Rc> { - use_memo(|_| RefCell::new(initial_value()), ()) +pub fn use_mut_ref(init_fn: F) -> Rc> +where + F: FnOnce() -> T, +{ + use_memo(|_| RefCell::new(initial_fn()), ()) } /// This hook is used for obtaining a [`NodeRef`]. @@ -114,5 +117,5 @@ pub fn use_mut_ref(initial_value: impl FnOnce() -> T) -> Rc NodeRef { - (*use_memo(|_| NodeRef::default(), ())).clone() + (*use_state(NodeRef::default)).clone() } diff --git a/packages/yew/src/functional/hooks/use_state.rs b/packages/yew/src/functional/hooks/use_state.rs index 130ff273cb7..5ce1d264138 100644 --- a/packages/yew/src/functional/hooks/use_state.rs +++ b/packages/yew/src/functional/hooks/use_state.rs @@ -55,9 +55,10 @@ where /// } /// ``` #[hook] -pub fn use_state(init_fn: impl FnOnce() -> T) -> UseStateHandle +pub fn use_state(init_fn: F) -> UseStateHandle where T: 'static, + F: FnOnce() -> T, { let handle = use_reducer(move || UseStateReducer { value: Rc::new(init_fn()), @@ -70,9 +71,10 @@ where /// /// This hook requires the state to implement [`PartialEq`]. #[hook] -pub fn use_state_eq(init_fn: impl FnOnce() -> T) -> UseStateHandle +pub fn use_state_eq(init_fn: F) -> UseStateHandle where T: PartialEq + 'static, + F: FnOnce() -> T, { let handle = use_reducer_eq(move || UseStateReducer { value: Rc::new(init_fn()), From 00a653ae2f7f21db377fba133c277a3ef8f7ae33 Mon Sep 17 00:00:00 2001 From: Kaede Hoshikawa Date: Thu, 27 Jan 2022 20:47:29 +0900 Subject: [PATCH 38/42] Fix typo. --- packages/yew/src/functional/hooks/use_ref.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/yew/src/functional/hooks/use_ref.rs b/packages/yew/src/functional/hooks/use_ref.rs index 81cc83a249f..045b9867d50 100644 --- a/packages/yew/src/functional/hooks/use_ref.rs +++ b/packages/yew/src/functional/hooks/use_ref.rs @@ -55,7 +55,7 @@ pub fn use_mut_ref(init_fn: F) -> Rc> where F: FnOnce() -> T, { - use_memo(|_| RefCell::new(initial_fn()), ()) + use_memo(|_| RefCell::new(init_fn()), ()) } /// This hook is used for obtaining a [`NodeRef`]. From c86123138bd2dcd03007731fc2a728395e315012 Mon Sep 17 00:00:00 2001 From: Kaede Hoshikawa Date: Thu, 27 Jan 2022 21:02:18 +0900 Subject: [PATCH 39/42] Remove more allocations. --- packages/yew/src/functional/hooks/mod.rs | 2 +- packages/yew/src/functional/hooks/use_effect.rs | 15 +++++++-------- packages/yew/src/functional/hooks/use_state.rs | 14 ++++---------- 3 files changed, 12 insertions(+), 19 deletions(-) diff --git a/packages/yew/src/functional/hooks/mod.rs b/packages/yew/src/functional/hooks/mod.rs index 776f66eca29..e45c93e423e 100644 --- a/packages/yew/src/functional/hooks/mod.rs +++ b/packages/yew/src/functional/hooks/mod.rs @@ -16,7 +16,7 @@ use crate::functional::{AnyScope, HookContext}; /// A trait that is implemented on hooks. /// -/// Hooks are defined via the [`#[hooks]`] macro. It provides rewrites to hook invocations +/// Hooks are defined via the [`#[hook]`](crate::functional::hook) macro. It provides rewrites to hook invocations /// and ensures that hooks can only be called at the top-level of a function component or a hook. /// Please refer to its documentation on how to implement hooks. pub trait Hook { diff --git a/packages/yew/src/functional/hooks/use_effect.rs b/packages/yew/src/functional/hooks/use_effect.rs index cade83f590a..9f58291ec97 100644 --- a/packages/yew/src/functional/hooks/use_effect.rs +++ b/packages/yew/src/functional/hooks/use_effect.rs @@ -1,5 +1,4 @@ use std::cell::RefCell; -use std::rc::Rc; use crate::functional::{hook, Effect, Hook, HookContext}; @@ -10,9 +9,9 @@ where D: FnOnce() + 'static, R: Fn(Option<&T>, Option<&T>) -> bool + 'static, { - runner_with_deps: Option<(Rc, F)>, + runner_with_deps: Option<(T, F)>, destructor: Option, - deps: Option>, + deps: Option, effect_changed_fn: R, } @@ -26,8 +25,8 @@ where fn rendered(&self) { let mut this = self.borrow_mut(); - if let Some((deps, callback)) = this.runner_with_deps.take() { - if !(this.effect_changed_fn)(Some(&*deps), this.deps.as_deref()) { + if let Some((deps, runner)) = this.runner_with_deps.take() { + if !(this.effect_changed_fn)(Some(&deps), this.deps.as_ref()) { return; } @@ -35,7 +34,7 @@ where de(); } - let new_destructor = callback(&deps); + let new_destructor = runner(&deps); this.deps = Some(deps); this.destructor = Some(new_destructor); @@ -74,7 +73,7 @@ where R: Fn(Option<&T>, Option<&T>) -> bool + 'static, { runner: F, - deps: Rc, + deps: T, effect_changed_fn: R, } @@ -109,7 +108,7 @@ where HookProvider { runner, - deps: Rc::new(deps), + deps, effect_changed_fn, } } diff --git a/packages/yew/src/functional/hooks/use_state.rs b/packages/yew/src/functional/hooks/use_state.rs index 5ce1d264138..0c543a78ed1 100644 --- a/packages/yew/src/functional/hooks/use_state.rs +++ b/packages/yew/src/functional/hooks/use_state.rs @@ -6,15 +6,13 @@ use super::{use_reducer, use_reducer_eq, Reducible, UseReducerDispatcher, UseRed use crate::functional::hook; struct UseStateReducer { - value: Rc, + value: T, } impl Reducible for UseStateReducer { type Action = T; fn reduce(self: Rc, action: Self::Action) -> Rc { - Rc::new(Self { - value: action.into(), - }) + Rc::new(Self { value: action }) } } @@ -60,9 +58,7 @@ where T: 'static, F: FnOnce() -> T, { - let handle = use_reducer(move || UseStateReducer { - value: Rc::new(init_fn()), - }); + let handle = use_reducer(move || UseStateReducer { value: init_fn() }); UseStateHandle { inner: handle } } @@ -76,9 +72,7 @@ where T: PartialEq + 'static, F: FnOnce() -> T, { - let handle = use_reducer_eq(move || UseStateReducer { - value: Rc::new(init_fn()), - }); + let handle = use_reducer_eq(move || UseStateReducer { value: init_fn() }); UseStateHandle { inner: handle } } From e82b4ecec8bc261018b7ad3bccedf809793d7428 Mon Sep 17 00:00:00 2001 From: Kaede Hoshikawa Date: Thu, 27 Jan 2022 21:10:31 +0900 Subject: [PATCH 40/42] Add comments. --- packages/yew/src/functional/hooks/use_reducer.rs | 2 ++ 1 file changed, 2 insertions(+) diff --git a/packages/yew/src/functional/hooks/use_reducer.rs b/packages/yew/src/functional/hooks/use_reducer.rs index 63c57943ba1..fe7a6c76db6 100644 --- a/packages/yew/src/functional/hooks/use_reducer.rs +++ b/packages/yew/src/functional/hooks/use_reducer.rs @@ -196,6 +196,8 @@ where should_render }; + // Currently, this triggers a render immediately, so we need to release the + // borrowed reference first. if should_render { re_render() } From 935a672f42d2af6b039e7bd3178eab4c86c9f96b Mon Sep 17 00:00:00 2001 From: Kaede Hoshikawa Date: Thu, 27 Jan 2022 22:04:15 +0900 Subject: [PATCH 41/42] Remove outdated comment. --- packages/yew/src/functional/hooks/use_reducer.rs | 1 - 1 file changed, 1 deletion(-) diff --git a/packages/yew/src/functional/hooks/use_reducer.rs b/packages/yew/src/functional/hooks/use_reducer.rs index fe7a6c76db6..6c98528b1eb 100644 --- a/packages/yew/src/functional/hooks/use_reducer.rs +++ b/packages/yew/src/functional/hooks/use_reducer.rs @@ -23,7 +23,6 @@ where { current_state: Rc>>, - // To be replaced with OnceCell once it becomes available in std. dispatch: DispatchFn, } From 806f61861e04ec3662daae9e1fbb905e6541af1a Mon Sep 17 00:00:00 2001 From: Kaede Hoshikawa Date: Thu, 27 Jan 2022 23:17:59 +0900 Subject: [PATCH 42/42] Bare Function Pointer for better code size. --- .../yew/src/functional/hooks/use_effect.rs | 23 ++++++++----------- .../yew/src/functional/hooks/use_reducer.rs | 10 ++++---- 2 files changed, 13 insertions(+), 20 deletions(-) diff --git a/packages/yew/src/functional/hooks/use_effect.rs b/packages/yew/src/functional/hooks/use_effect.rs index 9f58291ec97..dd470effe94 100644 --- a/packages/yew/src/functional/hooks/use_effect.rs +++ b/packages/yew/src/functional/hooks/use_effect.rs @@ -2,25 +2,23 @@ use std::cell::RefCell; use crate::functional::{hook, Effect, Hook, HookContext}; -struct UseEffectBase +struct UseEffectBase where F: FnOnce(&T) -> D + 'static, T: 'static, D: FnOnce() + 'static, - R: Fn(Option<&T>, Option<&T>) -> bool + 'static, { runner_with_deps: Option<(T, F)>, destructor: Option, deps: Option, - effect_changed_fn: R, + effect_changed_fn: fn(Option<&T>, Option<&T>) -> bool, } -impl Effect for RefCell> +impl Effect for RefCell> where F: FnOnce(&T) -> D + 'static, T: 'static, D: FnOnce() + 'static, - R: Fn(Option<&T>, Option<&T>) -> bool + 'static, { fn rendered(&self) { let mut this = self.borrow_mut(); @@ -42,12 +40,11 @@ where } } -impl Drop for UseEffectBase +impl Drop for UseEffectBase where F: FnOnce(&T) -> D + 'static, T: 'static, D: FnOnce() + 'static, - R: Fn(Option<&T>, Option<&T>) -> bool + 'static, { fn drop(&mut self) { if let Some(destructor) = self.destructor.take() { @@ -59,30 +56,28 @@ where fn use_effect_base( runner: impl FnOnce(&T) -> D + 'static, deps: T, - effect_changed_fn: impl Fn(Option<&T>, Option<&T>) -> bool + 'static, + effect_changed_fn: fn(Option<&T>, Option<&T>) -> bool, ) -> impl Hook where T: 'static, D: FnOnce() + 'static, { - struct HookProvider + struct HookProvider where F: FnOnce(&T) -> D + 'static, T: 'static, D: FnOnce() + 'static, - R: Fn(Option<&T>, Option<&T>) -> bool + 'static, { runner: F, deps: T, - effect_changed_fn: R, + effect_changed_fn: fn(Option<&T>, Option<&T>) -> bool, } - impl Hook for HookProvider + impl Hook for HookProvider where F: FnOnce(&T) -> D + 'static, T: 'static, D: FnOnce() + 'static, - R: Fn(Option<&T>, Option<&T>) -> bool + 'static, { type Output = (); @@ -93,7 +88,7 @@ where effect_changed_fn, } = self; - let state = ctx.next_effect(|_| -> RefCell> { + let state = ctx.next_effect(|_| -> RefCell> { RefCell::new(UseEffectBase { runner_with_deps: None, destructor: None, diff --git a/packages/yew/src/functional/hooks/use_reducer.rs b/packages/yew/src/functional/hooks/use_reducer.rs index 6c98528b1eb..1a89c7ad290 100644 --- a/packages/yew/src/functional/hooks/use_reducer.rs +++ b/packages/yew/src/functional/hooks/use_reducer.rs @@ -146,28 +146,26 @@ where /// The base function of [`use_reducer`] and [`use_reducer_eq`] fn use_reducer_base<'hook, T>( init_fn: impl 'hook + FnOnce() -> T, - should_render_fn: impl 'static + Fn(&T, &T) -> bool, + should_render_fn: fn(&T, &T) -> bool, ) -> impl 'hook + Hook> where T: Reducible + 'static, { - struct HookProvider<'hook, T, F, R> + struct HookProvider<'hook, T, F> where T: Reducible + 'static, F: 'hook + FnOnce() -> T, - R: 'static + Fn(&T, &T) -> bool, { _marker: PhantomData<&'hook ()>, init_fn: F, - should_render_fn: R, + should_render_fn: fn(&T, &T) -> bool, } - impl<'hook, T, F, R> Hook for HookProvider<'hook, T, F, R> + impl<'hook, T, F> Hook for HookProvider<'hook, T, F> where T: Reducible + 'static, F: 'hook + FnOnce() -> T, - R: 'static + Fn(&T, &T) -> bool, { type Output = UseReducerHandle;