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/Cargo.toml b/packages/yew-macro/Cargo.toml index cc99c648e0b..85fb249eff4 100644 --- a/packages/yew-macro/Cargo.toml +++ b/packages/yew-macro/Cargo.toml @@ -21,7 +21,9 @@ 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" +prettyplease = "0.1.1" # testing [dev-dependencies] 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 new file mode 100644 index 00000000000..ce24934ed20 --- /dev/null +++ b/packages/yew-macro/src/hook/body.rs @@ -0,0 +1,123 @@ +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::{ + parse_quote_spanned, visit_mut, Expr, ExprCall, ExprClosure, ExprForLoop, ExprIf, ExprLoop, + ExprMatch, ExprWhile, Ident, Item, +}; + +#[derive(Debug, Default)] +pub struct BodyRewriter { + branch_lock: Arc>, +} + +impl BodyRewriter { + fn is_branched(&self) -> bool { + self.branch_lock.try_lock().is_err() + } + + fn with_branch(&mut self, f: F) -> O + where + F: FnOnce(&mut BodyRewriter) -> O, + { + let branch_lock = self.branch_lock.clone(); + let _branched = branch_lock.try_lock(); + f(self) + } +} + +impl VisitMut for BodyRewriter { + fn visit_expr_call_mut(&mut self, i: &mut ExprCall) { + let ctx_ident = Ident::new("ctx", 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!( + 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) }; + } + + 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)); + } + + 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 new file mode 100644 index 00000000000..9ea6c19fc86 --- /dev/null +++ b/packages/yew-macro/src/hook/lifetime.rs @@ -0,0 +1,121 @@ +use proc_macro2::Span; +use std::sync::{Arc, Mutex}; +use syn::visit_mut::{self, VisitMut}; +use syn::{ + GenericArgument, Lifetime, ParenthesizedGenericArguments, Receiver, TypeBareFn, TypeImplTrait, + TypeParamBound, TypeReference, +}; + +// 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 impl_trait_lock: Arc>, + pub impl_fn_lock: Arc>, +} + +impl CollectLifetimes { + pub fn new(name: &'static str, default_span: Span) -> Self { + CollectLifetimes { + elided: Vec::new(), + explicit: Vec::new(), + name, + default_span, + + impl_trait_lock: Arc::default(), + impl_fn_lock: Arc::default(), + } + } + + fn is_impl_trait(&self) -> bool { + self.impl_trait_lock.try_lock().is_err() + } + + fn is_impl_fn(&self) -> bool { + self.impl_fn_lock.try_lock().is_err() + } + + 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) { + // We don't rewrite references in the impl FnOnce(&arg) or fn(&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) { + 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); + } + + fn visit_parenthesized_generic_arguments_mut( + &mut self, + generic_args: &mut ParenthesizedGenericArguments, + ) { + 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); + } + + 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 new file mode 100644 index 00000000000..ab7f3744a02 --- /dev/null +++ b/packages/yew-macro/src/hook/mod.rs @@ -0,0 +1,192 @@ +use proc_macro2::{Span, TokenStream}; +use proc_macro_error::emit_error; +use quote::quote; +use syn::parse::{Parse, ParseStream}; +use syn::visit_mut; +use syn::{parse_file, Ident, ItemFn, LitStr, ReturnType, Signature}; + +mod body; +mod lifetime; +mod signature; + +pub use body::BodyRewriter; +use signature::HookSignature; + +#[derive(Clone)] +pub struct HookFn { + inner: ItemFn, +} + +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, "async functions can't be hooks"); + } + + 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 }) + } +} + +pub fn hook_impl(component: HookFn) -> syn::Result { + let HookFn { inner: original_fn } = component; + + let ItemFn { + vis, + sig, + mut block, + attrs, + } = original_fn.clone(); + + 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#" +# Note + +When used in function components and hooks, this hook is equivalent to: + +``` +{} +``` +"#, + sig_formatted.replace( + "__yew_macro_dummy_function_body__", + "/* implementation omitted */" + ) + ), + Span::mixed_site(), + ); + + let hook_sig = HookSignature::rewrite(&sig); + + let Signature { + ref fn_token, + ref ident, + ref inputs, + output: ref hook_return_type, + ref generics, + .. + } = hook_sig.sig; + + let output_type = &hook_sig.output_type; + + let (impl_generics, ty_generics, where_clause) = generics.split_for_impl(); + + 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); + + let inner_fn_ident = Ident::new("inner_fn", Span::mixed_site()); + let input_args = hook_sig.input_args(); + + // there might be some overridden lifetimes in the return type. + let inner_fn_rt = match &sig.output { + ReturnType::Default => None, + ReturnType::Type(rarrow, _) => Some(quote! { #rarrow #output_type }), + }; + + 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 #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; + + ::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 { + _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,)*) + } + }; + + // 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 { + #inner_fn + + #inner_type_impl + } + + #[cfg(doctest)] + #original_fn + }; + + Ok(output) +} diff --git a/packages/yew-macro/src/hook/signature.rs b/packages/yew-macro/src/hook/signature.rs new file mode 100644 index 00000000000..a2031585e2f --- /dev/null +++ b/packages/yew-macro/src/hook/signature.rs @@ -0,0 +1,183 @@ +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, Receiver, + 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); + } + + 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 { + pub hook_lifetime: Lifetime, + pub sig: Signature, + pub output_type: Type, + pub needs_boxing: bool, +} + +impl HookSignature { + fn rewrite_return_type(hook_lifetime: &Lifetime, rt_type: &ReturnType) -> (ReturnType, Type) { + let bound = quote! { #hook_lifetime + }; + + 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 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 { + FnArg::Receiver(arg) => lifetimes.visit_receiver_mut(arg), + FnArg::Typed(arg) => lifetimes.visit_type_mut(&mut arg.ty), + } + } + + let Signature { + ref mut generics, + output: ref return_type, + .. + } = sig; + + let hook_lifetime = { + let hook_lifetime = Lifetime::new("'hook", Span::mixed_site()); + 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)); + } + + for explicit in lifetimes.explicit.iter() { + where_clause + .predicates + .push(parse_quote!(#explicit: #hook_lifetime)); + } + + for type_param in generics.type_params() { + let type_param_ident = &type_param.ident; + where_clause + .predicates + .push(parse_quote!(#type_param_ident: #hook_lifetime)); + } + + generics.where_clause = Some(where_clause); + + hook_lifetime + }; + + let (output, output_type) = Self::rewrite_return_type(&hook_lifetime, return_type); + sig.output = output; + + Self { + hook_lifetime, + sig, + output_type, + needs_boxing: arg_info.needs_boxing, + } + } + + pub fn phantom_types(&self) -> Vec { + self.sig + .generics + .type_params() + .map(|ty_param| ty_param.ident.clone()) + .collect() + } + + pub fn phantom_lifetimes(&self) -> Vec { + self.sig + .generics + .lifetimes() + .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() + } + + 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() + } +} diff --git a/packages/yew-macro/src/lib.rs b/packages/yew-macro/src/lib.rs index fe1d8940ae1..b01a9595c44 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; @@ -122,11 +124,9 @@ 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: 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 +134,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-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..37637ef7cac --- /dev/null +++ b/packages/yew-macro/tests/function_component_attr/hook_location-fail.rs @@ -0,0 +1,39 @@ +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(); + } + + while let Some(_m) = use_context::() { + 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.stderr b/packages/yew-macro/tests/function_component_attr/hook_location-fail.stderr new file mode 100644 index 00000000000..d40b4cfef08 --- /dev/null +++ b/packages/yew-macro/tests/function_component_attr/hook_location-fail.stderr @@ -0,0 +1,69 @@ +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 new file mode 100644 index 00000000000..b2bcbe111fb --- /dev/null +++ b/packages/yew-macro/tests/function_component_attr/hook_location-pass.rs @@ -0,0 +1,30 @@ +#![no_implicit_prelude] + +#[derive( + ::std::prelude::rust_2021::Debug, + ::std::prelude::rust_2021::PartialEq, + ::std::prelude::rust_2021::Clone, +)] +struct Ctx; + +#[::yew::prelude::function_component] +fn Comp() -> ::yew::prelude::Html { + ::yew::prelude::use_context::().unwrap(); + + if let ::std::prelude::rust_2021::Some(_m) = ::yew::prelude::use_context::() { + ::std::todo!() + } + + let _ctx = { ::yew::prelude::use_context::() }; + + match ::yew::prelude::use_context::() { + ::std::prelude::rust_2021::Some(_) => { + ::std::todo!() + } + ::std::prelude::rust_2021::None => { + ::std::todo!() + } + } +} + +fn main() {} 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 8b37ccb46dd..e45c93e423e 100644 --- a/packages/yew/src/functional/hooks/mod.rs +++ b/packages/yew/src/functional/hooks/mod.rs @@ -1,75 +1,64 @@ 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::*; -use crate::functional::{HookUpdater, CURRENT_HOOK}; -use std::cell::RefCell; -use std::ops::DerefMut; -use std::rc::Rc; +use crate::functional::{AnyScope, HookContext}; -/// Low level building block of creating hooks. +/// A trait that is implemented on 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. -/// -/// 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"); +/// 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 { + /// The return type when a hook is run. + type Output; + + /// Runs the hook inside current state, returns output upon completion. + 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> { + #[allow(missing_docs)] + pub fn new(inner: Box T>) -> Self { + Self { inner } } +} - // Extract current hook - let updater = CURRENT_HOOK.with(|hook_state| { - // Determine which hook position we're at and increment for the next hook - let hook_pos = hook_state.counter; - hook_state.counter += 1; +impl Hook for BoxedHook<'_, T> { + type Output = T; - // Initialize hook if this is the first call - if hook_pos >= 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()); - })); - } + fn run(self, ctx: &mut HookContext) -> Self::Output { + (self.inner)(ctx) + } +} - let hook = hook_state - .hooks - .get(hook_pos) - .expect("Not the same number of hooks. Hooks must not be called conditionally") - .clone(); +pub(crate) fn use_component_scope() -> impl Hook { + struct HookProvider {} - HookUpdater { - hook, - process_message: hook_state.process_message.clone(), - } - }); + impl Hook for HookProvider { + type Output = AnyScope; - // 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"); + fn run(self, ctx: &mut HookContext) -> Self::Output { + ctx.scope.clone() + } + } - runner(hook, updater.clone()) + HookProvider {} } diff --git a/packages/yew/src/functional/hooks/use_context.rs b/packages/yew/src/functional/hooks/use_context.rs index 581f4274264..24fb8fcda31 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::{get_current_scope, use_hook}; +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. @@ -28,38 +29,29 @@ use crate::functional::{get_current_scope, use_hook}; /// } /// } /// ``` +#[hook] pub fn use_context() -> Option { - struct UseContextState { - initialized: bool, - context: Option<(T2, ContextHandle)>, + struct UseContext { + context: Option<(T, ContextHandle)>, } - let scope = get_current_scope() - .expect("No current Scope. `use_context` can only be called inside function components"); + 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_memo( + move |_| UseContext { + context: scope.context::(Callback::from(move |m| { + val_dispatcher.clone().set(Some(m)); + })), + }, + (), + ) + }; - Some(state.context.as_ref()?.0.clone()) - }, - |state| { - state.context = None; - }, - ) + // 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_effect.rs b/packages/yew/src/functional/hooks/use_effect.rs index da68def20f3..dd470effe94 100644 --- a/packages/yew/src/functional/hooks/use_effect.rs +++ b/packages/yew/src/functional/hooks/use_effect.rs @@ -1,9 +1,111 @@ -use crate::functional::use_hook; -use std::rc::Rc; +use std::cell::RefCell; -struct UseEffect { - runner: Option Destructor>>, - destructor: Option>, +use crate::functional::{hook, Effect, Hook, HookContext}; + +struct UseEffectBase +where + F: FnOnce(&T) -> D + 'static, + T: 'static, + D: FnOnce() + 'static, +{ + runner_with_deps: Option<(T, F)>, + destructor: Option, + deps: Option, + effect_changed_fn: fn(Option<&T>, Option<&T>) -> bool, +} + +impl Effect for RefCell> +where + F: FnOnce(&T) -> D + 'static, + T: 'static, + D: FnOnce() + 'static, +{ + fn rendered(&self) { + let mut this = self.borrow_mut(); + + if let Some((deps, runner)) = this.runner_with_deps.take() { + if !(this.effect_changed_fn)(Some(&deps), this.deps.as_ref()) { + return; + } + + if let Some(de) = this.destructor.take() { + de(); + } + + let new_destructor = runner(&deps); + + this.deps = Some(deps); + this.destructor = Some(new_destructor); + } + } +} + +impl Drop for UseEffectBase +where + F: FnOnce(&T) -> D + 'static, + T: 'static, + D: FnOnce() + 'static, +{ + fn drop(&mut self) { + if let Some(destructor) = self.destructor.take() { + destructor() + } + } +} + +fn use_effect_base( + runner: impl FnOnce(&T) -> D + 'static, + deps: T, + effect_changed_fn: fn(Option<&T>, Option<&T>) -> bool, +) -> impl Hook +where + T: 'static, + D: FnOnce() + 'static, +{ + struct HookProvider + where + F: FnOnce(&T) -> D + 'static, + T: 'static, + D: FnOnce() + 'static, + { + runner: F, + deps: T, + effect_changed_fn: fn(Option<&T>, Option<&T>) -> bool, + } + + impl Hook for HookProvider + where + F: FnOnce(&T) -> D + 'static, + T: 'static, + D: FnOnce() + 'static, + { + type Output = (); + + fn run(self, ctx: &mut HookContext) -> Self::Output { + let Self { + runner, + 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, runner)); + } + } + + HookProvider { + runner, + deps, + effect_changed_fn, + } } /// This hook is used for hooking into the component's lifecycle. @@ -36,51 +138,13 @@ struct UseEffect { /// } /// } /// ``` -pub fn use_effect(callback: impl FnOnce() -> Destructor + 'static) +#[hook] +pub fn use_effect(f: F) where - Destructor: FnOnce() + 'static, + F: FnOnce() -> D + '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(|_| f(), (), |_, _| true); } /// This hook is similar to [`use_effect`] but it accepts dependencies. @@ -88,48 +152,12 @@ 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. -pub fn use_effect_with_deps(callback: Callback, deps: Dependents) +#[hook] +pub fn use_effect_with_deps(f: F, deps: T) where - Callback: FnOnce(&Dependents) -> Destructor + 'static, - Destructor: FnOnce() + 'static, - Dependents: PartialEq + 'static, + T: PartialEq + 'static, + F: FnOnce(&T) -> D + '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(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 new file mode 100644 index 00000000000..f7538c23e64 --- /dev/null +++ b/packages/yew/src/functional/hooks/use_memo.rs @@ -0,0 +1,37 @@ +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 +#[hook] +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) }); + let last_deps = use_state(|| -> RefCell> { RefCell::new(None) }); + + let mut val = val.borrow_mut(); + let mut last_deps = last_deps.borrow_mut(); + + 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(f(&deps)); + *last_deps = Some(deps); + + *val = Some(new_val.clone()); + + new_val + } + } +} diff --git a/packages/yew/src/functional/hooks/use_reducer.rs b/packages/yew/src/functional/hooks/use_reducer.rs index 99ea78c3e67..1a89c7ad290 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::use_hook; +use crate::functional::{hook, Hook, HookContext}; type DispatchFn = Rc::Action)>; @@ -20,10 +21,9 @@ 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,51 +144,76 @@ where } /// The base function of [`use_reducer`] and [`use_reducer_eq`] -fn use_reducer_base(initial_fn: F, should_render_fn: R) -> UseReducerHandle +fn use_reducer_base<'hook, T>( + init_fn: impl 'hook + FnOnce() -> T, + should_render_fn: fn(&T, &T) -> bool, +) -> impl 'hook + Hook> where T: Reducible + 'static, - F: FnOnce() -> T, - R: (Fn(&T, &T) -> bool) + '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> + where + T: Reducible + 'static, + F: 'hook + FnOnce() -> T, + { + _marker: PhantomData<&'hook ()>, - should_render - }); - }); + init_fn: F, + should_render_fn: fn(&T, &T) -> bool, + } - *dispatch_ref = Some(dispatch.clone()); + impl<'hook, T, F> Hook for HookProvider<'hook, T, F> + where + T: Reducible + 'static, + F: 'hook + FnOnce() -> T, + { + type Output = UseReducerHandle; - dispatch + fn run(self, ctx: &mut HookContext) -> Self::Output { + let Self { + init_fn, + should_render_fn, + .. + } = self; + + let state = ctx.next_state(move |re_render| { + let val = Rc::new(RefCell::new(Rc::new(init_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 + }; + + // Currently, this triggers a render immediately, so we need to release the + // borrowed reference first. + 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, + init_fn, + should_render_fn, + } } /// This hook is an alternative to [`use_state`](super::use_state()). @@ -259,22 +284,24 @@ where /// } /// } /// ``` -pub fn use_reducer(initial_fn: F) -> UseReducerHandle +#[hook] +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`. /// /// This requires the state to implement [`PartialEq`] in addition to the [`Reducible`] trait /// required by [`use_reducer`]. -pub fn use_reducer_eq(initial_fn: F) -> UseReducerHandle +#[hook] +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 d334a4bbee2..045b9867d50 100644 --- a/packages/yew/src/functional/hooks/use_ref.rs +++ b/packages/yew/src/functional/hooks/use_ref.rs @@ -1,5 +1,8 @@ -use crate::{functional::use_hook, NodeRef}; -use std::{cell::RefCell, rc::Rc}; +use std::cell::RefCell; +use std::rc::Rc; + +use crate::functional::{hook, use_memo, use_state}; +use crate::NodeRef; /// This hook is used for obtaining a mutable reference to a stateful value. /// Its state persists across renders. @@ -47,25 +50,12 @@ use std::{cell::RefCell, rc::Rc}; /// } /// } /// ``` -pub fn use_mut_ref(initial_value: impl 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), - |_| {}, - ) +#[hook] +pub fn use_mut_ref(init_fn: F) -> Rc> +where + F: FnOnce() -> T, +{ + use_memo(|_| RefCell::new(init_fn()), ()) } /// This hook is used for obtaining a [`NodeRef`]. @@ -88,7 +78,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 @@ -125,6 +115,7 @@ pub fn use_ref(initial_value: impl FnOnce() -> T) -> Rc { /// } /// /// ``` +#[hook] pub fn use_node_ref() -> NodeRef { - use_hook(NodeRef::default, |state, _| state.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 ae3f7f0fd5e..0c543a78ed1 100644 --- a/packages/yew/src/functional/hooks/use_state.rs +++ b/packages/yew/src/functional/hooks/use_state.rs @@ -3,17 +3,16 @@ use std::ops::Deref; use std::rc::Rc; use super::{use_reducer, use_reducer_eq, Reducible, UseReducerDispatcher, UseReducerHandle}; +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 }) } } @@ -53,14 +52,13 @@ where /// } /// } /// ``` +#[hook] 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()), - }); + let handle = use_reducer(move || UseStateReducer { value: init_fn() }); UseStateHandle { inner: handle } } @@ -68,14 +66,13 @@ where /// [`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, 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 } } diff --git a/packages/yew/src/functional/mod.rs b/packages/yew/src/functional/mod.rs index 5b23411f9e0..4d3f587ec36 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 scoped_tls_hkt::scoped_thread_local; +use std::any::Any; use std::cell::RefCell; use std::fmt; use std::rc::Rc; @@ -55,17 +55,71 @@ use crate::html::SealedBaseComponent; /// ``` pub use yew_macro::function_component; -scoped_thread_local!(static mut CURRENT_HOOK: HookState); +/// 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; + +/// Primitives of a Hook state. +pub(crate) trait Effect { + fn rendered(&self) {} +} + +/// A hook context to be passed to hooks. +pub struct HookContext { + pub(crate) scope: AnyScope, + re_render: ReRender, + + states: Vec>, + effects: Vec>, -struct HookState { counter: usize, - scope: AnyScope, - process_message: ProcessMessage, - hooks: Vec>>, - destroy_listeners: Vec>, + #[cfg(debug_assertions)] + total_hook_counter: Option, +} + +impl HookContext { + pub(crate) fn next_state(&mut self, initializer: impl FnOnce(ReRender) -> T) -> Rc + where + T: 'static, + { + // Determine which hook position we're at and increment for the next hook + let hook_pos = self.counter; + self.counter += 1; + + let state = match self.states.get(hook_pos).cloned() { + Some(m) => m, + None => { + let initial_state = Rc::new(initializer(self.re_render.clone())); + self.states.push(initial_state.clone()); + + initial_state + } + }; + + state.downcast().unwrap() + } + + pub(crate) fn next_effect(&mut self, initializer: impl FnOnce(ReRender) -> T) -> Rc + where + T: 'static + Effect, + { + let prev_state_len = self.states.len(); + let t = self.next_state(initializer); + + // This is a new effect, we add it to effects. + if self.states.len() != prev_state_len { + self.effects.push(t.clone()); + } + + t + } +} + +impl fmt::Debug for HookContext { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + f.write_str("HookContext<_>") + } } /// Trait that allows a struct to act as Function Component. @@ -76,14 +130,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, - message_queue: MsgQueue, + hook_ctx: RefCell, } impl fmt::Debug for FunctionComponent { @@ -92,52 +145,36 @@ 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, { - 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(); Self { _never: std::marker::PhantomData::default(), - message_queue: message_queue.clone(), - hook_state: RefCell::new(HookState { - counter: 0, + hook_ctx: RefCell::new(HookContext { + effects: Vec::new(), 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![], - destroy_listeners: vec![], + states: Vec::new(), + + counter: 0, + #[cfg(debug_assertions)] + total_hook_counter: None, }), } } - 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 { @@ -145,107 +182,58 @@ 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(); + ctx.counter = 0; + + #[allow(clippy::let_and_return)] + let result = T::run(&mut *ctx, props); + + #[cfg(debug_assertions)] + { + // Procedural Macros can catch most conditionally called hooks at compile time, but it cannot + // detect early return (as the return can be Err(_), Suspension). + if result.is_err() { + if let Some(m) = ctx.total_hook_counter { + // Suspended Components can have less hooks called when suspended, but not more. + if m < ctx.counter { + panic!("Hooks are called conditionally."); + } + } + } else { + match ctx.total_hook_counter { + Some(m) => { + if m != ctx.counter { + panic!("Hooks are called conditionally."); + } + } + None => { + ctx.total_hook_counter = Some(ctx.counter); + } + } + } + } + + 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) { + let hook_ctx = self.hook_ctx.borrow(); + + for effect in hook_ctx.effects.iter() { + effect.rendered(); } } fn destroy(&mut self, _ctx: &Context) { - let mut hook_state = self.hook_state.borrow_mut(); - for hook in hook_state.destroy_listeners.drain(..) { - hook() - } - } -} + let mut hook_ctx = self.hook_ctx.borrow_mut(); + // We clear the effects as these are also references to states. + hook_ctx.effects.clear(); -pub(crate) fn get_current_scope() -> Option { - if CURRENT_HOOK.is_set() { - Some(CURRENT_HOOK.with(|state| state.scope.clone())) - } else { - None + 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. -/// -/// 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)] -#[allow(missing_debug_implementations)] -pub struct HookUpdater { - hook: Rc>, - process_message: ProcessMessage, -} - -impl HookUpdater { - /// Callback which runs the hook. - 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(); - - // Update the component - // We're calling "link.send_message", so we're not calling it post-render - let post_render = false; - 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) - }), - post_render, - ); - } - - /// Callback called 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(); - - // Update the component - // We're calling "message_queue.push", so not calling it post-render - let post_render = true; - 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) - }), - post_render, - ); - } -} 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/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 4fffc969e94..0f655c39692 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,61 +11,51 @@ 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 { + let example_context = use_context::(); + + if example_context.is_some() { + console_log!( + "Context should be None here, but was {:?}!", + example_context + ); + }; + 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; + 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::(); + html! { +
{ &context.unwrap().0 }
} } @@ -86,83 +73,70 @@ 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 { + let ctx_a = use_context::(); + let ctx_b = use_context::(); - fn run(_props: &Self::TProps) -> HtmlResult { - assert_eq!(use_context::(), Some(ContextA(2))); - assert_eq!(use_context::(), Some(ContextB(1))); + assert_eq!(ctx_a, Some(ContextA(2))); + assert_eq!(ctx_b, Some(ContextB(1))); - Ok(html! {}) - } + html! {} } - type Test1 = FunctionComponent; - struct Test2Function; - impl FunctionProvider for Test2Function { - type TProps = (); + #[function_component] + fn Test2() -> Html { + let ctx_a = use_context::(); + let ctx_b = use_context::(); - fn run(_props: &Self::TProps) -> HtmlResult { - assert_eq!(use_context::(), Some(ContextA(0))); - assert_eq!(use_context::(), Some(ContextB(1))); + assert_eq!(ctx_a, Some(ContextA(0))); + assert_eq!(ctx_b, Some(ContextB(1))); - Ok(html! {}) - } + html! {} } - type Test2 = FunctionComponent; - struct Test3Function; - impl FunctionProvider for Test3Function { - type TProps = (); + #[function_component] + fn Test3() -> Html { + let ctx_a = use_context::(); + let ctx_b = use_context::(); - fn run(_props: &Self::TProps) -> HtmlResult { - assert_eq!(use_context::(), Some(ContextA(0))); - assert_eq!(use_context::(), None); + assert_eq!(ctx_a, Some(ContextA(0))); + assert_eq!(ctx_b, None); - Ok(html! {}) - } + html! {} } - type Test3 = FunctionComponent; - struct Test4Function; - impl FunctionProvider for Test4Function { - type TProps = (); + #[function_component] + fn Test4() -> Html { + let ctx_a = use_context::(); + let ctx_b = use_context::(); - fn run(_props: &Self::TProps) -> HtmlResult { - assert_eq!(use_context::(), None); - assert_eq!(use_context::(), None); + assert_eq!(ctx_a, None); + assert_eq!(ctx_b, None); - Ok(html! {}) - } + 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; + + html! { +
+ + + + + + + + + + +
} } - type TestComponent = FunctionComponent; yew::start_app_in_element::( gloo_utils::document().get_element_by_id("output").unwrap(), @@ -180,24 +154,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; + html! { + <> +
+ { format!("total: {}", counter.borrow()) } +
+ { props.children.clone() } + } } - type RenderCounter = FunctionComponent; #[derive(Clone, Debug, PartialEq, Properties)] struct ContextOutletProps { @@ -205,75 +174,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"); + + 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; + } + _ => (), + }; + || {} + }); + } + 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..594d607dcb7 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() + }, + (), + ); + 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)) }; + html! { + + } + } else { + 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, + ); + + 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") + }, + (), + ); + + 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, + ); + 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_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"); +} diff --git a/packages/yew/tests/use_reducer.rs b/packages/yew/tests/use_reducer.rs index de81203edd6..2d6454bc735 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); + || {} + }, + (), + ); + 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())); + + 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..662f09206a6 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) + } + 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..1dd59b1de69 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(UseComponent)] + fn use_state_comp() -> Html { + let counter = use_state(|| 0); + if *counter < 5 { + counter.set(*counter + 1) + } + 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(UseComponent)] + 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(); + 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 = (); + #[function_component(UseComponent)] + fn use_state_comp() -> Html { + RENDER_COUNT.fetch_add(1, Ordering::Relaxed); + let counter = use_state_eq(|| 0); + counter.set(1); - fn run(_: &Self::TProps) -> HtmlResult { - RENDER_COUNT.fetch_add(1, Ordering::Relaxed); - let counter = use_state_eq(|| 0); - counter.set(1); - - Ok(html! { -
- {"Test Output: "} -
{*counter}
- {"\n"} -
- }) + html! { +
+ {"Test Output: "} +
{*counter}
+ {"\n"} +
} } - type UseComponent = FunctionComponent; + yew::start_app_in_element::( gloo_utils::document().get_element_by_id("output").unwrap(), ); diff --git a/website/docs/concepts/function-components/custom-hooks.mdx b/website/docs/concepts/function-components/custom-hooks.mdx index 7dbd3e7b217..95445b1b56f 100644 --- a/website/docs/concepts/function-components/custom-hooks.mdx +++ b/website/docs/concepts/function-components/custom-hooks.mdx @@ -39,13 +39,16 @@ 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; +use yew::prelude::*; +#[hook] pub fn use_event(target: &EventTarget, event_type: E, callback: F) where E: Into>, @@ -65,6 +68,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..64098c07238 100644 --- a/website/docs/concepts/function-components/introduction.mdx +++ b/website/docs/concepts/function-components/introduction.mdx @@ -40,12 +40,18 @@ 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 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 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..5402ebfa66d 100644 --- a/website/docs/concepts/function-components/pre-defined-hooks.mdx +++ b/website/docs/concepts/function-components/pre-defined-hooks.mdx @@ -64,22 +64,29 @@ 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 memoized value. Its state persists across renders. +Its value will be recalculated only if any of the dependencies values change. -`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, Properties}; -#[function_component(UseRef)] -fn ref_hook() -> Html { - let message = use_ref(|| "Some Expensive State.".to_string()); +#[derive(PartialEq, Properties)] +pub struct Props { + pub step: usize, +} + +#[function_component(UseMemo)] +fn ref_hook(props: &Props) -> Html { + // Will only get recalculated if `props.step` value changes + let message = use_memo( + |step| format!("{}. Do Some Expensive Calculation", step), + props.step + ); html! {
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). 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..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 @@ -6,3 +6,12 @@ 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). + +## 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 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.