Function Components & Hooks V2 (#2401)

* Make a use_hook hook with the new Hook trait.

* Implement Lifetime.

* Rewrites function signature.

* Only apply lifetime if there're other lifetimes.

* Cleanup signature rewrite logic.

* Rewrite hook body.

* Port some built-in hooks.

* Finish porting all built-in hooks.

* Port tests.

* Fix tests.

* Migrate to macro-based hooks.

* Fix HookContext, add tests on non-possible locations.

* Fix stderr for trybuild.

* Add 1 more test case.

* Adjust doc location.

* Pretty print hook signature.

* Fix Items & std::ops::Fn*.

* Add use_memo.

* Optimise Implementation of hooks.

* Use Box to capture function value only.

* Detect whether needs boxing.

* Add args if boxing not needed.

* Enforce hook number.

* Deduplicate use_effect.

* Optimise Implementation.

* Update documentation.

* Fix website test. Strip BoxedHook implementation from it.

* Allow doc string.

* Workaround doc tests.

* Optimise codebase & documentation.

* Fix website test.

* Reduce implementation complexity.

* Destructor is no more.

* Documentation and macros.

* Reduce heap allocation and hook complexity.

* Remove Queue as well.

* Prefer Generics.

* Fix typo.

* Remove more allocations.

* Add comments.

* Remove outdated comment.

* Bare Function Pointer for better code size.
This commit is contained in:
Kaede Hoshikawa 2022-01-28 18:51:37 +09:00 committed by GitHub
parent 22f3f46dd7
commit 485a1b8c4a
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
39 changed files with 1741 additions and 909 deletions

View File

@ -1,6 +1,6 @@
use std::ops::Deref; use std::ops::Deref;
use std::rc::Rc; use std::rc::Rc;
use yew::{use_state_eq, UseStateHandle}; use yew::prelude::*;
#[derive(Clone)] #[derive(Clone)]
pub struct UseBoolToggleHandle { pub struct UseBoolToggleHandle {
@ -47,6 +47,7 @@ impl Deref for UseBoolToggleHandle {
/// <button {onclick}>{ "Click me" }</button> /// <button {onclick}>{ "Click me" }</button>
/// ... /// ...
/// ``` /// ```
#[hook]
pub fn use_bool_toggle(default: bool) -> UseBoolToggleHandle { pub fn use_bool_toggle(default: bool) -> UseBoolToggleHandle {
let state = use_state_eq(|| default); let state = use_state_eq(|| default);

View File

@ -57,6 +57,7 @@ impl PartialEq for UuidState {
} }
} }
#[hook]
fn use_random_uuid() -> SuspensionResult<Uuid> { fn use_random_uuid() -> SuspensionResult<Uuid> {
let s = use_state(UuidState::new); let s = use_state(UuidState::new);

View File

@ -28,6 +28,7 @@ impl Reducible for SleepState {
} }
} }
#[hook]
pub fn use_sleep() -> SuspensionResult<Rc<dyn Fn()>> { pub fn use_sleep() -> SuspensionResult<Rc<dyn Fn()>> {
let sleep_state = use_reducer(SleepState::new); let sleep_state = use_reducer(SleepState::new);

View File

@ -29,6 +29,7 @@ where
/// ///
/// Takes a callback as the only argument. The callback will be updated on every render to make /// 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. /// sure captured values (if any) are up to date.
#[hook]
pub fn use_bridge<T, F>(on_output: F) -> UseBridgeHandle<T> pub fn use_bridge<T, F>(on_output: F) -> UseBridgeHandle<T>
where where
T: Bridged, T: Bridged,

View File

@ -21,7 +21,9 @@ lazy_static = "1"
proc-macro-error = "1" proc-macro-error = "1"
proc-macro2 = "1" proc-macro2 = "1"
quote = "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 # testing
[dev-dependencies] [dev-dependencies]

View File

@ -3,7 +3,11 @@ use quote::{format_ident, quote, ToTokens};
use syn::parse::{Parse, ParseStream}; use syn::parse::{Parse, ParseStream};
use syn::punctuated::Punctuated; use syn::punctuated::Punctuated;
use syn::token::{Comma, Fn}; 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)] #[derive(Clone)]
pub struct FunctionComponent { pub struct FunctionComponent {
@ -169,7 +173,7 @@ fn print_fn(func_comp: FunctionComponent, use_fn_name: bool) -> TokenStream {
fn_token, fn_token,
name, name,
attrs, attrs,
block, mut block,
return_type, return_type,
generics, generics,
arg, arg,
@ -184,9 +188,14 @@ fn print_fn(func_comp: FunctionComponent, use_fn_name: bool) -> TokenStream {
Ident::new("inner", Span::mixed_site()) 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! { quote! {
#(#attrs)* #(#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 #where_clause
{ {
#block #block
@ -241,6 +250,8 @@ pub fn function_component_impl(
Ident::new("inner", Span::mixed_site()) Ident::new("inner", Span::mixed_site())
}; };
let ctx_ident = Ident::new("ctx", Span::mixed_site());
let quoted = quote! { let quoted = quote! {
#[doc(hidden)] #[doc(hidden)]
#[allow(non_camel_case_types)] #[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 { impl #impl_generics ::yew::functional::FunctionProvider for #provider_name #ty_generics #where_clause {
type TProps = #props_type; 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 #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))
} }
} }

View File

@ -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<Mutex<()>>,
}
impl BodyRewriter {
fn is_branched(&self) -> bool {
self.branch_lock.try_lock().is_err()
}
fn with_branch<F, O>(&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.
}
}

View File

@ -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<Lifetime>,
pub explicit: Vec<Lifetime>,
pub name: &'static str,
pub default_span: Span,
pub impl_trait_lock: Arc<Mutex<()>>,
pub impl_fn_lock: Arc<Mutex<()>>,
}
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<Lifetime>) {
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<S: Into<Option<Span>>>(&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);
}
}

View File

@ -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<Self> {
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<TokenStream> {
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<dyn #hook_lifetime_plus FnOnce(&mut ::yew::functional::HookContext) #inner_fn_rt> };
// 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)
}

View File

@ -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<Output = ()> },
parse_quote! { () },
),
ReturnType::Type(arrow, ref return_type) => (
parse_quote_spanned! {
return_type.span() => #arrow impl #bound ::yew::functional::Hook<Output = #return_type>
},
*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<Ident> {
self.sig
.generics
.type_params()
.map(|ty_param| ty_param.ident.clone())
.collect()
}
pub fn phantom_lifetimes(&self) -> Vec<TypeReference> {
self.sig
.generics
.lifetimes()
.map(|life| parse_quote! { &#life () })
.collect()
}
pub fn input_args(&self) -> Vec<Ident> {
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<Type> {
self.sig
.inputs
.iter()
.filter_map(|m| {
if let FnArg::Typed(m) = m {
return Some(*m.ty.clone());
}
None
})
.collect()
}
}

View File

@ -49,12 +49,14 @@
mod classes; mod classes;
mod derive_props; mod derive_props;
mod function_component; mod function_component;
mod hook;
mod html_tree; mod html_tree;
mod props; mod props;
mod stringify; mod stringify;
use derive_props::DerivePropsInput; use derive_props::DerivePropsInput;
use function_component::{function_component_impl, FunctionComponent, FunctionComponentName}; use function_component::{function_component_impl, FunctionComponent, FunctionComponentName};
use hook::{hook_impl, HookFn};
use html_tree::{HtmlRoot, HtmlRootVNode}; use html_tree::{HtmlRoot, HtmlRootVNode};
use proc_macro::TokenStream; use proc_macro::TokenStream;
use quote::ToTokens; use quote::ToTokens;
@ -122,11 +124,9 @@ pub fn classes(input: TokenStream) -> TokenStream {
TokenStream::from(classes.into_token_stream()) TokenStream::from(classes.into_token_stream())
} }
#[proc_macro_error::proc_macro_error]
#[proc_macro_attribute] #[proc_macro_attribute]
pub fn function_component( pub fn function_component(attr: TokenStream, item: TokenStream) -> proc_macro::TokenStream {
attr: proc_macro::TokenStream,
item: proc_macro::TokenStream,
) -> proc_macro::TokenStream {
let item = parse_macro_input!(item as FunctionComponent); let item = parse_macro_input!(item as FunctionComponent);
let attr = parse_macro_input!(attr as FunctionComponentName); let attr = parse_macro_input!(attr as FunctionComponentName);
@ -134,3 +134,19 @@ pub fn function_component(
.unwrap_or_else(|err| err.to_compile_error()) .unwrap_or_else(|err| err.to_compile_error())
.into() .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()
}

View File

@ -29,36 +29,36 @@ error[E0277]: the trait bound `FunctionComponent<CompFunctionProvider<MissingTyp
<FunctionComponent<T> as BaseComponent> <FunctionComponent<T> as BaseComponent>
error[E0599]: the function or associated item `new` exists for struct `VChild<FunctionComponent<CompFunctionProvider<MissingTypeBounds>>>`, but its trait bounds were not satisfied error[E0599]: the function or associated item `new` exists for struct `VChild<FunctionComponent<CompFunctionProvider<MissingTypeBounds>>>`, but its trait bounds were not satisfied
--> tests/function_component_attr/generic-props-fail.rs:27:14 --> tests/function_component_attr/generic-props-fail.rs:27:14
| |
27 | html! { <Comp<MissingTypeBounds> /> }; 27 | html! { <Comp<MissingTypeBounds> /> };
| ^^^^ function or associated item cannot be called on `VChild<FunctionComponent<CompFunctionProvider<MissingTypeBounds>>>` due to unsatisfied trait bounds | ^^^^ function or associated item cannot be called on `VChild<FunctionComponent<CompFunctionProvider<MissingTypeBounds>>>` due to unsatisfied trait bounds
| |
::: $WORKSPACE/packages/yew/src/functional/mod.rs ::: $WORKSPACE/packages/yew/src/functional/mod.rs
| |
| pub struct FunctionComponent<T: FunctionProvider + 'static> { | pub struct FunctionComponent<T: FunctionProvider + 'static> {
| ----------------------------------------------------------- doesn't satisfy `_: BaseComponent` | ----------------------------------------------------------- doesn't satisfy `_: BaseComponent`
| |
= note: the following trait bounds were not satisfied: = note: the following trait bounds were not satisfied:
`FunctionComponent<CompFunctionProvider<MissingTypeBounds>>: BaseComponent` `FunctionComponent<CompFunctionProvider<MissingTypeBounds>>: BaseComponent`
error[E0277]: the trait bound `MissingTypeBounds: yew::Properties` is not satisfied error[E0277]: the trait bound `MissingTypeBounds: yew::Properties` is not satisfied
--> tests/function_component_attr/generic-props-fail.rs:27:14 --> tests/function_component_attr/generic-props-fail.rs:27:14
| |
27 | html! { <Comp<MissingTypeBounds> /> }; 27 | html! { <Comp<MissingTypeBounds> /> };
| ^^^^ the trait `yew::Properties` is not implemented for `MissingTypeBounds` | ^^^^ the trait `yew::Properties` is not implemented for `MissingTypeBounds`
| |
note: required because of the requirements on the impl of `FunctionProvider` for `CompFunctionProvider<MissingTypeBounds>` note: required because of the requirements on the impl of `FunctionProvider` for `CompFunctionProvider<MissingTypeBounds>`
--> tests/function_component_attr/generic-props-fail.rs:8:1 --> tests/function_component_attr/generic-props-fail.rs:8:1
| |
8 | #[function_component(Comp)] 8 | #[function_component(Comp)]
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^ | ^^^^^^^^^^^^^^^^^^^^^^^^^^^
note: required by a bound in `FunctionComponent` note: required by a bound in `FunctionComponent`
--> $WORKSPACE/packages/yew/src/functional/mod.rs --> $WORKSPACE/packages/yew/src/functional/mod.rs
| |
| pub struct FunctionComponent<T: FunctionProvider + 'static> { | pub struct FunctionComponent<T: FunctionProvider + 'static> {
| ^^^^^^^^^^^^^^^^ required by this bound in `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) = 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` error[E0107]: missing generics for type alias `Comp`
--> tests/function_component_attr/generic-props-fail.rs:30:14 --> tests/function_component_attr/generic-props-fail.rs:30:14

View File

@ -0,0 +1,39 @@
use yew::prelude::*;
#[derive(Debug, PartialEq, Clone)]
struct Ctx;
#[function_component]
fn Comp() -> Html {
if let Some(_m) = use_context::<Ctx>() {
use_context::<Ctx>().unwrap();
todo!()
}
let _ = || {
use_context::<Ctx>().unwrap();
todo!()
};
for _ in 0..10 {
use_context::<Ctx>().unwrap();
}
while let Some(_m) = use_context::<Ctx>() {
use_context::<Ctx>().unwrap();
}
match use_context::<Ctx>() {
Some(_) => use_context::<Ctx>(),
None => {
todo!()
}
}
loop {
use_context::<Ctx>().unwrap();
todo!()
}
}
fn main() {}

View File

@ -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::<Ctx>().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::<Ctx>().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::<Ctx>().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::<Ctx>() {
| ^^^^^^^^^^^
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::<Ctx>().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::<Ctx>(),
| ^^^^^^^^^^^
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::<Ctx>().unwrap();
| ^^^^^^^^^^^

View File

@ -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::<Ctx>().unwrap();
if let ::std::prelude::rust_2021::Some(_m) = ::yew::prelude::use_context::<Ctx>() {
::std::todo!()
}
let _ctx = { ::yew::prelude::use_context::<Ctx>() };
match ::yew::prelude::use_context::<Ctx>() {
::std::prelude::rust_2021::Some(_) => {
::std::todo!()
}
::std::prelude::rust_2021::None => {
::std::todo!()
}
}
}
fn main() {}

View File

@ -8,11 +8,13 @@ use crate::router::{LocationContext, NavigatorContext};
use yew::prelude::*; use yew::prelude::*;
/// A hook to access the [`Navigator`]. /// A hook to access the [`Navigator`].
#[hook]
pub fn use_navigator() -> Option<Navigator> { pub fn use_navigator() -> Option<Navigator> {
use_context::<NavigatorContext>().map(|m| m.navigator()) use_context::<NavigatorContext>().map(|m| m.navigator())
} }
/// A hook to access the current [`Location`]. /// A hook to access the current [`Location`].
#[hook]
pub fn use_location() -> Option<Location> { pub fn use_location() -> Option<Location> {
Some(use_context::<LocationContext>()?.location()) Some(use_context::<LocationContext>()?.location())
} }
@ -25,6 +27,7 @@ pub fn use_location() -> Option<Location> {
/// ///
/// If your `Routable` has a `#[not_found]` route, you can use `.unwrap_or_default()` instead of /// If your `Routable` has a `#[not_found]` route, you can use `.unwrap_or_default()` instead of
/// `.unwrap()` to unwrap. /// `.unwrap()` to unwrap.
#[hook]
pub fn use_route<R>() -> Option<R> pub fn use_route<R>() -> Option<R>
where where
R: Routable + 'static, R: Routable + 'static,

View File

@ -27,8 +27,6 @@ wasm-bindgen = "0.2"
yew-macro = { version = "^0.19.0", path = "../yew-macro" } yew-macro = { version = "^0.19.0", path = "../yew-macro" }
thiserror = "1.0" thiserror = "1.0"
scoped-tls-hkt = "0.1"
futures = { version = "0.3", optional = true } futures = { version = "0.3", optional = true }
html-escape = { version = "0.2.9", optional = true } html-escape = { version = "0.2.9", optional = true }

View File

@ -1,75 +1,64 @@
mod use_context; mod use_context;
mod use_effect; mod use_effect;
mod use_memo;
mod use_reducer; mod use_reducer;
mod use_ref; mod use_ref;
mod use_state; mod use_state;
pub use use_context::*; pub use use_context::*;
pub use use_effect::*; pub use use_effect::*;
pub use use_memo::*;
pub use use_reducer::*; pub use use_reducer::*;
pub use use_ref::*; pub use use_ref::*;
pub use use_state::*; pub use use_state::*;
use crate::functional::{HookUpdater, CURRENT_HOOK}; use crate::functional::{AnyScope, HookContext};
use std::cell::RefCell;
use std::ops::DerefMut;
use std::rc::Rc;
/// Low level building block of creating hooks. /// A trait that is implemented on hooks.
/// ///
/// It is used to created the pre-defined primitive hooks. /// Hooks are defined via the [`#[hook]`](crate::functional::hook) macro. It provides rewrites to hook invocations
/// Generally, it isn't needed to create hooks and should be avoided as most custom hooks can be /// and ensures that hooks can only be called at the top-level of a function component or a hook.
/// created by combining other hooks as described in [Yew Docs]. /// Please refer to its documentation on how to implement hooks.
/// pub trait Hook {
/// The `initializer` callback is called once to create the initial state of the hook. /// The return type when a hook is run.
/// `runner` callback handles the logic of the hook. It is called when the hook function is called. type Output;
/// `destructor`, as the name implies, is called to cleanup the leftovers of the hook.
/// /// Runs the hook inside current state, returns output upon completion.
/// See the pre-defined hooks for examples of how to use this function. fn run(self, ctx: &mut HookContext) -> Self::Output;
/// }
/// [Yew Docs]: https://yew.rs/next/concepts/function-components/custom-hooks
pub fn use_hook<InternalHook: 'static, Output, Tear: FnOnce(&mut InternalHook) + 'static>( /// The blanket implementation of boxed hooks.
initializer: impl FnOnce() -> InternalHook, #[doc(hidden)]
runner: impl FnOnce(&mut InternalHook, HookUpdater) -> Output, #[allow(missing_debug_implementations, missing_docs)]
destructor: Tear, pub struct BoxedHook<'hook, T> {
) -> Output { inner: Box<dyn 'hook + FnOnce(&mut HookContext) -> T>,
if !CURRENT_HOOK.is_set() { }
panic!("Hooks can only be used in the scope of a function component");
impl<'hook, T> BoxedHook<'hook, T> {
#[allow(missing_docs)]
pub fn new(inner: Box<dyn 'hook + FnOnce(&mut HookContext) -> T>) -> Self {
Self { inner }
}
}
impl<T> Hook for BoxedHook<'_, T> {
type Output = T;
fn run(self, ctx: &mut HookContext) -> Self::Output {
(self.inner)(ctx)
}
}
pub(crate) fn use_component_scope() -> impl Hook<Output = AnyScope> {
struct HookProvider {}
impl Hook for HookProvider {
type Output = AnyScope;
fn run(self, ctx: &mut HookContext) -> Self::Output {
ctx.scope.clone()
}
} }
// Extract current hook HookProvider {}
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;
// 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());
}));
}
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(),
}
});
// 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())
} }

View File

@ -1,5 +1,6 @@
use crate::callback::Callback;
use crate::context::ContextHandle; 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. /// 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. /// 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<T: Clone + PartialEq + 'static>() -> Option<T> { pub fn use_context<T: Clone + PartialEq + 'static>() -> Option<T> {
struct UseContextState<T2: Clone + PartialEq + 'static> { struct UseContext<T: Clone + PartialEq + 'static> {
initialized: bool, context: Option<(T, ContextHandle<T>)>,
context: Option<(T2, ContextHandle<T2>)>,
} }
let scope = get_current_scope() let scope = use_component_scope();
.expect("No current Scope. `use_context` can only be called inside function components");
use_hook( let val = use_state(|| -> Option<T> { None });
move || UseContextState { let state = {
initialized: false, let val_dispatcher = val.setter();
context: None, use_memo(
}, move |_| UseContext {
|state: &mut UseContextState<T>, updater| { context: scope.context::<T>(Callback::from(move |m| {
if !state.initialized { val_dispatcher.clone().set(Some(m));
state.initialized = true; })),
let callback = move |ctx: T| { },
updater.callback(|state: &mut UseContextState<T>| { (),
if let Some(context) = &mut state.context { )
context.0 = ctx; };
}
true
});
};
state.context = scope.context::<T>(callback.into());
}
Some(state.context.as_ref()?.0.clone()) // we fallback to initial value if it was not updated.
}, (*val)
|state| { .clone()
state.context = None; .or_else(move || state.context.as_ref().map(|m| m.0.clone()))
},
)
} }

View File

@ -1,9 +1,111 @@
use crate::functional::use_hook; use std::cell::RefCell;
use std::rc::Rc;
struct UseEffect<Destructor> { use crate::functional::{hook, Effect, Hook, HookContext};
runner: Option<Box<dyn FnOnce() -> Destructor>>,
destructor: Option<Box<Destructor>>, struct UseEffectBase<T, F, D>
where
F: FnOnce(&T) -> D + 'static,
T: 'static,
D: FnOnce() + 'static,
{
runner_with_deps: Option<(T, F)>,
destructor: Option<D>,
deps: Option<T>,
effect_changed_fn: fn(Option<&T>, Option<&T>) -> bool,
}
impl<T, F, D> Effect for RefCell<UseEffectBase<T, F, D>>
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<T, F, D> Drop for UseEffectBase<T, F, D>
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<T, D>(
runner: impl FnOnce(&T) -> D + 'static,
deps: T,
effect_changed_fn: fn(Option<&T>, Option<&T>) -> bool,
) -> impl Hook<Output = ()>
where
T: 'static,
D: FnOnce() + 'static,
{
struct HookProvider<T, F, D>
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<T, F, D> Hook for HookProvider<T, F, D>
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<UseEffectBase<T, F, D>> {
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. /// This hook is used for hooking into the component's lifecycle.
@ -36,51 +138,13 @@ struct UseEffect<Destructor> {
/// } /// }
/// } /// }
/// ``` /// ```
pub fn use_effect<Destructor>(callback: impl FnOnce() -> Destructor + 'static) #[hook]
pub fn use_effect<F, D>(f: F)
where where
Destructor: FnOnce() + 'static, F: FnOnce() -> D + 'static,
D: FnOnce() + 'static,
{ {
use_hook( use_effect_base(|_| f(), (), |_, _| true);
move || {
let effect: UseEffect<Destructor> = UseEffect {
runner: None,
destructor: None,
};
effect
},
|state, updater| {
state.runner = Some(Box::new(callback) as Box<dyn FnOnce() -> Destructor>);
// Run on every render
updater.post_render(move |state: &mut UseEffect<Destructor>| {
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<Dependents, Destructor> = Box<dyn FnOnce(&Dependents) -> Destructor>;
struct UseEffectDeps<Destructor, Dependents> {
runner_with_deps: Option<(
Rc<Dependents>,
UseEffectDepsRunnerFn<Dependents, Destructor>,
)>,
destructor: Option<Box<Destructor>>,
deps: Option<Rc<Dependents>>,
} }
/// This hook is similar to [`use_effect`] but it accepts dependencies. /// This hook is similar to [`use_effect`] but it accepts dependencies.
@ -88,48 +152,12 @@ struct UseEffectDeps<Destructor, Dependents> {
/// Whenever the dependencies are changed, the effect callback is called again. /// Whenever the dependencies are changed, the effect callback is called again.
/// To detect changes, dependencies must implement `PartialEq`. /// To detect changes, dependencies must implement `PartialEq`.
/// Note that the destructor also runs when dependencies change. /// Note that the destructor also runs when dependencies change.
pub fn use_effect_with_deps<Callback, Destructor, Dependents>(callback: Callback, deps: Dependents) #[hook]
pub fn use_effect_with_deps<T, F, D>(f: F, deps: T)
where where
Callback: FnOnce(&Dependents) -> Destructor + 'static, T: PartialEq + 'static,
Destructor: FnOnce() + 'static, F: FnOnce(&T) -> D + 'static,
Dependents: PartialEq + 'static, D: FnOnce() + 'static,
{ {
let deps = Rc::new(deps); use_effect_base(f, deps, |lhs, rhs| lhs != rhs)
use_hook(
move || {
let destructor: Option<Box<Destructor>> = 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<Destructor, Dependents>| {
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()
}
},
);
} }

View File

@ -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<T, F, D>(f: F, deps: D) -> Rc<T>
where
T: 'static,
F: FnOnce(&D) -> T,
D: 'static + PartialEq,
{
let val = use_state(|| -> RefCell<Option<Rc<T>>> { RefCell::new(None) });
let last_deps = use_state(|| -> RefCell<Option<D>> { 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
}
}
}

View File

@ -1,9 +1,10 @@
use std::cell::RefCell; use std::cell::RefCell;
use std::fmt; use std::fmt;
use std::marker::PhantomData;
use std::ops::Deref; use std::ops::Deref;
use std::rc::Rc; use std::rc::Rc;
use crate::functional::use_hook; use crate::functional::{hook, Hook, HookContext};
type DispatchFn<T> = Rc<dyn Fn(<T as Reducible>::Action)>; type DispatchFn<T> = Rc<dyn Fn(<T as Reducible>::Action)>;
@ -20,10 +21,9 @@ struct UseReducer<T>
where where
T: Reducible, T: Reducible,
{ {
current_state: Rc<T>, current_state: Rc<RefCell<Rc<T>>>,
// To be replaced with OnceCell once it becomes available in std. dispatch: DispatchFn<T>,
dispatch: RefCell<Option<DispatchFn<T>>>,
} }
/// State handle for [`use_reducer`] and [`use_reducer_eq`] hook /// 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`] /// The base function of [`use_reducer`] and [`use_reducer_eq`]
fn use_reducer_base<T, F, R>(initial_fn: F, should_render_fn: R) -> UseReducerHandle<T> fn use_reducer_base<'hook, T>(
init_fn: impl 'hook + FnOnce() -> T,
should_render_fn: fn(&T, &T) -> bool,
) -> impl 'hook + Hook<Output = UseReducerHandle<T>>
where where
T: Reducible + 'static, T: Reducible + 'static,
F: FnOnce() -> T,
R: (Fn(&T, &T) -> bool) + 'static,
{ {
use_hook( struct HookProvider<'hook, T, F>
move || UseReducer { where
current_state: Rc::new(initial_fn()), T: Reducible + 'static,
dispatch: RefCell::default(), F: 'hook + FnOnce() -> T,
}, {
|s, updater| { _marker: PhantomData<&'hook ()>,
let mut dispatch_ref = s.dispatch.borrow_mut();
// Create dispatch once. init_fn: F,
let dispatch = match *dispatch_ref { should_render_fn: fn(&T, &T) -> bool,
Some(ref m) => (*m).to_owned(), }
None => {
let should_render_fn = Rc::new(should_render_fn);
let dispatch: Rc<dyn Fn(T::Action)> = Rc::new(move |action: T::Action| { impl<'hook, T, F> Hook for HookProvider<'hook, T, F>
let should_render_fn = should_render_fn.clone(); where
T: Reducible + 'static,
F: 'hook + FnOnce() -> T,
{
type Output = UseReducerHandle<T>;
updater.callback(move |state: &mut UseReducer<T>| { fn run(self, ctx: &mut HookContext) -> Self::Output {
let next_state = state.current_state.clone().reduce(action); let Self {
let should_render = should_render_fn(&next_state, &state.current_state); init_fn,
state.current_state = next_state; 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 should_render
}); };
});
*dispatch_ref = Some(dispatch.clone()); // Currently, this triggers a render immediately, so we need to release the
// borrowed reference first.
dispatch if should_render {
re_render()
}
}),
} }
}; });
UseReducerHandle { let value = state.current_state.borrow().clone();
value: Rc::clone(&s.current_state), let dispatch = state.dispatch.clone();
dispatch,
} UseReducerHandle { value, dispatch }
}, }
|_| {}, }
)
HookProvider {
_marker: PhantomData,
init_fn,
should_render_fn,
}
} }
/// This hook is an alternative to [`use_state`](super::use_state()). /// This hook is an alternative to [`use_state`](super::use_state()).
@ -259,22 +284,24 @@ where
/// } /// }
/// } /// }
/// ``` /// ```
pub fn use_reducer<T, F>(initial_fn: F) -> UseReducerHandle<T> #[hook]
pub fn use_reducer<T, F>(init_fn: F) -> UseReducerHandle<T>
where where
T: Reducible + 'static, T: Reducible + 'static,
F: FnOnce() -> T, 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`. /// [`use_reducer`] but only re-renders when `prev_state != next_state`.
/// ///
/// This requires the state to implement [`PartialEq`] in addition to the [`Reducible`] trait /// This requires the state to implement [`PartialEq`] in addition to the [`Reducible`] trait
/// required by [`use_reducer`]. /// required by [`use_reducer`].
pub fn use_reducer_eq<T, F>(initial_fn: F) -> UseReducerHandle<T> #[hook]
pub fn use_reducer_eq<T, F>(init_fn: F) -> UseReducerHandle<T>
where where
T: Reducible + PartialEq + 'static, T: Reducible + PartialEq + 'static,
F: FnOnce() -> T, F: FnOnce() -> T,
{ {
use_reducer_base(initial_fn, T::ne) use_reducer_base(init_fn, T::ne)
} }

View File

@ -1,5 +1,8 @@
use crate::{functional::use_hook, NodeRef}; use std::cell::RefCell;
use std::{cell::RefCell, rc::Rc}; 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. /// This hook is used for obtaining a mutable reference to a stateful value.
/// Its state persists across renders. /// Its state persists across renders.
@ -47,25 +50,12 @@ use std::{cell::RefCell, rc::Rc};
/// } /// }
/// } /// }
/// ``` /// ```
pub fn use_mut_ref<T: 'static>(initial_value: impl FnOnce() -> T) -> Rc<RefCell<T>> { #[hook]
use_hook( pub fn use_mut_ref<T: 'static, F>(init_fn: F) -> Rc<RefCell<T>>
|| Rc::new(RefCell::new(initial_value())), where
|state, _| state.clone(), F: FnOnce() -> T,
|_| {}, {
) use_memo(|_| RefCell::new(init_fn()), ())
}
/// 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<T: 'static>(initial_value: impl FnOnce() -> T) -> Rc<T> {
use_hook(
|| Rc::new(initial_value()),
|state, _| Rc::clone(state),
|_| {},
)
} }
/// This hook is used for obtaining a [`NodeRef`]. /// This hook is used for obtaining a [`NodeRef`].
@ -125,6 +115,7 @@ pub fn use_ref<T: 'static>(initial_value: impl FnOnce() -> T) -> Rc<T> {
/// } /// }
/// ///
/// ``` /// ```
#[hook]
pub fn use_node_ref() -> NodeRef { pub fn use_node_ref() -> NodeRef {
use_hook(NodeRef::default, |state, _| state.clone(), |_| {}) (*use_state(NodeRef::default)).clone()
} }

View File

@ -3,17 +3,16 @@ use std::ops::Deref;
use std::rc::Rc; use std::rc::Rc;
use super::{use_reducer, use_reducer_eq, Reducible, UseReducerDispatcher, UseReducerHandle}; use super::{use_reducer, use_reducer_eq, Reducible, UseReducerDispatcher, UseReducerHandle};
use crate::functional::hook;
struct UseStateReducer<T> { struct UseStateReducer<T> {
value: Rc<T>, value: T,
} }
impl<T> Reducible for UseStateReducer<T> { impl<T> Reducible for UseStateReducer<T> {
type Action = T; type Action = T;
fn reduce(self: Rc<Self>, action: Self::Action) -> Rc<Self> { fn reduce(self: Rc<Self>, action: Self::Action) -> Rc<Self> {
Rc::new(Self { Rc::new(Self { value: action })
value: action.into(),
})
} }
} }
@ -53,14 +52,13 @@ where
/// } /// }
/// } /// }
/// ``` /// ```
#[hook]
pub fn use_state<T, F>(init_fn: F) -> UseStateHandle<T> pub fn use_state<T, F>(init_fn: F) -> UseStateHandle<T>
where where
T: 'static, T: 'static,
F: FnOnce() -> T, F: FnOnce() -> T,
{ {
let handle = use_reducer(move || UseStateReducer { let handle = use_reducer(move || UseStateReducer { value: init_fn() });
value: Rc::new(init_fn()),
});
UseStateHandle { inner: handle } UseStateHandle { inner: handle }
} }
@ -68,14 +66,13 @@ where
/// [`use_state`] but only re-renders when `prev_state != next_state`. /// [`use_state`] but only re-renders when `prev_state != next_state`.
/// ///
/// This hook requires the state to implement [`PartialEq`]. /// This hook requires the state to implement [`PartialEq`].
#[hook]
pub fn use_state_eq<T, F>(init_fn: F) -> UseStateHandle<T> pub fn use_state_eq<T, F>(init_fn: F) -> UseStateHandle<T>
where where
T: PartialEq + 'static, T: PartialEq + 'static,
F: FnOnce() -> T, F: FnOnce() -> T,
{ {
let handle = use_reducer_eq(move || UseStateReducer { let handle = use_reducer_eq(move || UseStateReducer { value: init_fn() });
value: Rc::new(init_fn()),
});
UseStateHandle { inner: handle } UseStateHandle { inner: handle }
} }

View File

@ -15,7 +15,7 @@
use crate::html::{AnyScope, BaseComponent, HtmlResult}; use crate::html::{AnyScope, BaseComponent, HtmlResult};
use crate::Properties; use crate::Properties;
use scoped_tls_hkt::scoped_thread_local; use std::any::Any;
use std::cell::RefCell; use std::cell::RefCell;
use std::fmt; use std::fmt;
use std::rc::Rc; use std::rc::Rc;
@ -55,17 +55,71 @@ use crate::html::SealedBaseComponent;
/// ``` /// ```
pub use yew_macro::function_component; 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<dyn FnOnce() -> bool>; type ReRender = Rc<dyn Fn()>;
type ProcessMessage = Rc<dyn Fn(Msg, bool)>;
/// 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<Rc<dyn Any>>,
effects: Vec<Rc<dyn Effect>>,
struct HookState {
counter: usize, counter: usize,
scope: AnyScope, #[cfg(debug_assertions)]
process_message: ProcessMessage, total_hook_counter: Option<usize>,
hooks: Vec<Rc<RefCell<dyn std::any::Any>>>, }
destroy_listeners: Vec<Box<dyn FnOnce()>>,
impl HookContext {
pub(crate) fn next_state<T>(&mut self, initializer: impl FnOnce(ReRender) -> T) -> Rc<T>
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<T>(&mut self, initializer: impl FnOnce(ReRender) -> T) -> Rc<T>
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. /// 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. /// Render the component. This function returns the [`Html`](crate::Html) to be rendered for the component.
/// ///
/// Equivalent of [`Component::view`](crate::html::Component::view). /// 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. /// Wrapper that allows a struct implementing [`FunctionProvider`] to be consumed as a component.
pub struct FunctionComponent<T: FunctionProvider + 'static> { pub struct FunctionComponent<T: FunctionProvider + 'static> {
_never: std::marker::PhantomData<T>, _never: std::marker::PhantomData<T>,
hook_state: RefCell<HookState>, hook_ctx: RefCell<HookContext>,
message_queue: MsgQueue,
} }
impl<T: FunctionProvider> fmt::Debug for FunctionComponent<T> { impl<T: FunctionProvider> fmt::Debug for FunctionComponent<T> {
@ -92,52 +145,36 @@ impl<T: FunctionProvider> fmt::Debug for FunctionComponent<T> {
} }
} }
impl<T> FunctionComponent<T>
where
T: FunctionProvider,
{
fn with_hook_state<R>(&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<T> BaseComponent for FunctionComponent<T> impl<T> BaseComponent for FunctionComponent<T>
where where
T: FunctionProvider + 'static, T: FunctionProvider + 'static,
{ {
type Message = Box<dyn FnOnce() -> bool>; type Message = ();
type Properties = T::TProps; type Properties = T::TProps;
fn create(ctx: &Context<Self>) -> Self { fn create(ctx: &Context<Self>) -> Self {
let scope = AnyScope::from(ctx.link().clone()); let scope = AnyScope::from(ctx.link().clone());
let message_queue = MsgQueue::default();
Self { Self {
_never: std::marker::PhantomData::default(), _never: std::marker::PhantomData::default(),
message_queue: message_queue.clone(), hook_ctx: RefCell::new(HookContext {
hook_state: RefCell::new(HookState { effects: Vec::new(),
counter: 0,
scope, scope,
process_message: { re_render: {
let scope = ctx.link().clone(); let link = ctx.link().clone();
Rc::new(move |msg, post_render| { Rc::new(move || link.send_message(()))
if post_render {
message_queue.push(msg);
} else {
scope.send_message(msg);
}
})
}, },
hooks: vec![], states: Vec::new(),
destroy_listeners: vec![],
counter: 0,
#[cfg(debug_assertions)]
total_hook_counter: None,
}), }),
} }
} }
fn update(&mut self, _ctx: &Context<Self>, msg: Self::Message) -> bool { fn update(&mut self, _ctx: &Context<Self>, _msg: Self::Message) -> bool {
msg() true
} }
fn changed(&mut self, _ctx: &Context<Self>) -> bool { fn changed(&mut self, _ctx: &Context<Self>) -> bool {
@ -145,107 +182,58 @@ where
} }
fn view(&self, ctx: &Context<Self>) -> HtmlResult { fn view(&self, ctx: &Context<Self>) -> 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<Self>, _first_render: bool) { fn rendered(&mut self, _ctx: &Context<Self>, _first_render: bool) {
for msg in self.message_queue.drain() { let hook_ctx = self.hook_ctx.borrow();
ctx.link().send_message(msg);
for effect in hook_ctx.effects.iter() {
effect.rendered();
} }
} }
fn destroy(&mut self, _ctx: &Context<Self>) { fn destroy(&mut self, _ctx: &Context<Self>) {
let mut hook_state = self.hook_state.borrow_mut(); let mut hook_ctx = self.hook_ctx.borrow_mut();
for hook in hook_state.destroy_listeners.drain(..) { // We clear the effects as these are also references to states.
hook() hook_ctx.effects.clear();
for state in hook_ctx.states.drain(..) {
drop(state);
} }
} }
} }
pub(crate) fn get_current_scope() -> Option<AnyScope> {
if CURRENT_HOOK.is_set() {
Some(CURRENT_HOOK.with(|state| state.scope.clone()))
} else {
None
}
}
impl<T> SealedBaseComponent for FunctionComponent<T> where T: FunctionProvider + 'static {} impl<T> SealedBaseComponent for FunctionComponent<T> where T: FunctionProvider + 'static {}
#[derive(Clone, Default)]
struct MsgQueue(Rc<RefCell<Vec<Msg>>>);
impl MsgQueue {
fn push(&self, msg: Msg) {
self.0.borrow_mut().push(msg);
}
fn drain(&self) -> Vec<Msg> {
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<RefCell<dyn std::any::Any>>,
process_message: ProcessMessage,
}
impl HookUpdater {
/// Callback which runs the hook.
pub fn callback<T: 'static, F>(&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<T: 'static, F>(&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,
);
}
}

View File

@ -206,6 +206,7 @@ mod ssr_tests {
} }
} }
#[hook]
pub fn use_sleep() -> SuspensionResult<Rc<dyn Fn()>> { pub fn use_sleep() -> SuspensionResult<Rc<dyn Fn()>> {
let sleep_state = use_reducer(SleepState::new); let sleep_state = use_reducer(SleepState::new);

View File

@ -2,31 +2,27 @@ mod common;
use common::obtain_result; use common::obtain_result;
use wasm_bindgen_test::*; use wasm_bindgen_test::*;
use yew::functional::{FunctionComponent, FunctionProvider}; use yew::prelude::*;
use yew::{html, HtmlResult, Properties};
wasm_bindgen_test::wasm_bindgen_test_configure!(run_in_browser); wasm_bindgen_test::wasm_bindgen_test_configure!(run_in_browser);
#[wasm_bindgen_test] #[wasm_bindgen_test]
fn props_are_passed() { fn props_are_passed() {
struct PropsPassedFunction {}
#[derive(Properties, Clone, PartialEq)] #[derive(Properties, Clone, PartialEq)]
struct PropsPassedFunctionProps { struct PropsPassedFunctionProps {
value: String, value: String,
} }
impl FunctionProvider for PropsPassedFunction {
type TProps = PropsPassedFunctionProps;
fn run(props: &Self::TProps) -> HtmlResult { #[function_component]
assert_eq!(&props.value, "props"); fn PropsComponent(props: &PropsPassedFunctionProps) -> Html {
return Ok(html! { assert_eq!(&props.value, "props");
<div id="result"> html! {
{"done"} <div id="result">
</div> {"done"}
}); </div>
} }
} }
type PropsComponent = FunctionComponent<PropsPassedFunction>;
yew::start_app_with_props_in_element::<PropsComponent>( yew::start_app_with_props_in_element::<PropsComponent>(
gloo_utils::document().get_element_by_id("output").unwrap(), gloo_utils::document().get_element_by_id("output").unwrap(),
PropsPassedFunctionProps { PropsPassedFunctionProps {

View File

@ -44,6 +44,7 @@ async fn suspense_works() {
} }
} }
#[hook]
pub fn use_sleep() -> SuspensionResult<Rc<dyn Fn()>> { pub fn use_sleep() -> SuspensionResult<Rc<dyn Fn()>> {
let sleep_state = use_reducer(SleepState::new); let sleep_state = use_reducer(SleepState::new);
@ -182,6 +183,7 @@ async fn suspense_not_suspended_at_start() {
} }
} }
#[hook]
pub fn use_sleep() -> SuspensionResult<Rc<dyn Fn()>> { pub fn use_sleep() -> SuspensionResult<Rc<dyn Fn()>> {
let sleep_state = use_reducer(SleepState::new); let sleep_state = use_reducer(SleepState::new);
@ -297,6 +299,7 @@ async fn suspense_nested_suspense_works() {
} }
} }
#[hook]
pub fn use_sleep() -> SuspensionResult<Rc<dyn Fn()>> { pub fn use_sleep() -> SuspensionResult<Rc<dyn Fn()>> {
let sleep_state = use_reducer(SleepState::new); let sleep_state = use_reducer(SleepState::new);
@ -430,6 +433,7 @@ async fn effects_not_run_when_suspended() {
} }
} }
#[hook]
pub fn use_sleep() -> SuspensionResult<Rc<dyn Fn()>> { pub fn use_sleep() -> SuspensionResult<Rc<dyn Fn()>> {
let sleep_state = use_reducer(SleepState::new); let sleep_state = use_reducer(SleepState::new);

View File

@ -3,10 +3,7 @@ mod common;
use common::obtain_result_by_id; use common::obtain_result_by_id;
use std::rc::Rc; use std::rc::Rc;
use wasm_bindgen_test::*; use wasm_bindgen_test::*;
use yew::functional::{ use yew::prelude::*;
use_context, use_effect, use_mut_ref, use_state, FunctionComponent, FunctionProvider,
};
use yew::{html, Children, ContextProvider, HtmlResult, Properties};
wasm_bindgen_test::wasm_bindgen_test_configure!(run_in_browser); 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() { fn use_context_scoping_works() {
#[derive(Clone, Debug, PartialEq)] #[derive(Clone, Debug, PartialEq)]
struct ExampleContext(String); struct ExampleContext(String);
struct UseContextFunctionOuter {}
struct UseContextFunctionInner {}
struct ExpectNoContextFunction {}
type UseContextComponent = FunctionComponent<UseContextFunctionOuter>;
type UseContextComponentInner = FunctionComponent<UseContextFunctionInner>;
type ExpectNoContextComponent = FunctionComponent<ExpectNoContextFunction>;
impl FunctionProvider for ExpectNoContextFunction {
type TProps = ();
fn run(_props: &Self::TProps) -> HtmlResult { #[function_component]
if use_context::<ExampleContext>().is_some() { fn ExpectNoContextComponent() -> Html {
console_log!( let example_context = use_context::<ExampleContext>();
"Context should be None here, but was {:?}!",
use_context::<ExampleContext>().unwrap() if example_context.is_some() {
); console_log!(
}; "Context should be None here, but was {:?}!",
Ok(html! { example_context
<div></div> );
}) };
html! {
<div></div>
} }
} }
impl FunctionProvider for UseContextFunctionOuter {
type TProps = ();
fn run(_props: &Self::TProps) -> HtmlResult { #[function_component]
type ExampleContextProvider = ContextProvider<ExampleContext>; fn UseContextComponent() -> Html {
Ok(html! { type ExampleContextProvider = ContextProvider<ExampleContext>;
<div> html! {
<ExampleContextProvider context={ExampleContext("wrong1".into())}> <div>
<div>{"ignored"}</div> <ExampleContextProvider context={ExampleContext("wrong1".into())}>
</ExampleContextProvider> <div>{"ignored"}</div>
<ExampleContextProvider context={ExampleContext("wrong2".into())}> </ExampleContextProvider>
<ExampleContextProvider context={ExampleContext("correct".into())}> <ExampleContextProvider context={ExampleContext("wrong2".into())}>
<ExampleContextProvider context={ExampleContext("wrong1".into())}> <ExampleContextProvider context={ExampleContext("correct".into())}>
<div>{"ignored"}</div> <ExampleContextProvider context={ExampleContext("wrong1".into())}>
</ExampleContextProvider> <div>{"ignored"}</div>
<UseContextComponentInner />
</ExampleContextProvider> </ExampleContextProvider>
<UseContextComponentInner />
</ExampleContextProvider> </ExampleContextProvider>
<ExampleContextProvider context={ExampleContext("wrong3".into())}> </ExampleContextProvider>
<div>{"ignored"}</div> <ExampleContextProvider context={ExampleContext("wrong3".into())}>
</ExampleContextProvider> <div>{"ignored"}</div>
<ExpectNoContextComponent /> </ExampleContextProvider>
</div> <ExpectNoContextComponent />
}) </div>
} }
} }
impl FunctionProvider for UseContextFunctionInner {
type TProps = ();
fn run(_props: &Self::TProps) -> HtmlResult { #[function_component]
let context = use_context::<ExampleContext>(); fn UseContextComponentInner() -> Html {
Ok(html! { let context = use_context::<ExampleContext>();
<div id="result">{ &context.unwrap().0 }</div> html! {
}) <div id="result">{ &context.unwrap().0 }</div>
} }
} }
@ -86,83 +73,70 @@ fn use_context_works_with_multiple_types() {
#[derive(Clone, Debug, PartialEq)] #[derive(Clone, Debug, PartialEq)]
struct ContextB(u32); struct ContextB(u32);
struct Test1Function; #[function_component]
impl FunctionProvider for Test1Function { fn Test1() -> Html {
type TProps = (); let ctx_a = use_context::<ContextA>();
let ctx_b = use_context::<ContextB>();
fn run(_props: &Self::TProps) -> HtmlResult { assert_eq!(ctx_a, Some(ContextA(2)));
assert_eq!(use_context::<ContextA>(), Some(ContextA(2))); assert_eq!(ctx_b, Some(ContextB(1)));
assert_eq!(use_context::<ContextB>(), Some(ContextB(1)));
Ok(html! {}) html! {}
}
#[function_component]
fn Test2() -> Html {
let ctx_a = use_context::<ContextA>();
let ctx_b = use_context::<ContextB>();
assert_eq!(ctx_a, Some(ContextA(0)));
assert_eq!(ctx_b, Some(ContextB(1)));
html! {}
}
#[function_component]
fn Test3() -> Html {
let ctx_a = use_context::<ContextA>();
let ctx_b = use_context::<ContextB>();
assert_eq!(ctx_a, Some(ContextA(0)));
assert_eq!(ctx_b, None);
html! {}
}
#[function_component]
fn Test4() -> Html {
let ctx_a = use_context::<ContextA>();
let ctx_b = use_context::<ContextB>();
assert_eq!(ctx_a, None);
assert_eq!(ctx_b, None);
html! {}
}
#[function_component]
fn TestComponent() -> Html {
type ContextAProvider = ContextProvider<ContextA>;
type ContextBProvider = ContextProvider<ContextB>;
html! {
<div>
<ContextAProvider context={ContextA(0)}>
<ContextBProvider context={ContextB(1)}>
<ContextAProvider context={ContextA(2)}>
<Test1/>
</ContextAProvider>
<Test2/>
</ContextBProvider>
<Test3/>
</ContextAProvider>
<Test4 />
</div>
} }
} }
type Test1 = FunctionComponent<Test1Function>;
struct Test2Function;
impl FunctionProvider for Test2Function {
type TProps = ();
fn run(_props: &Self::TProps) -> HtmlResult {
assert_eq!(use_context::<ContextA>(), Some(ContextA(0)));
assert_eq!(use_context::<ContextB>(), Some(ContextB(1)));
Ok(html! {})
}
}
type Test2 = FunctionComponent<Test2Function>;
struct Test3Function;
impl FunctionProvider for Test3Function {
type TProps = ();
fn run(_props: &Self::TProps) -> HtmlResult {
assert_eq!(use_context::<ContextA>(), Some(ContextA(0)));
assert_eq!(use_context::<ContextB>(), None);
Ok(html! {})
}
}
type Test3 = FunctionComponent<Test3Function>;
struct Test4Function;
impl FunctionProvider for Test4Function {
type TProps = ();
fn run(_props: &Self::TProps) -> HtmlResult {
assert_eq!(use_context::<ContextA>(), None);
assert_eq!(use_context::<ContextB>(), None);
Ok(html! {})
}
}
type Test4 = FunctionComponent<Test4Function>;
struct TestFunction;
impl FunctionProvider for TestFunction {
type TProps = ();
fn run(_props: &Self::TProps) -> HtmlResult {
type ContextAProvider = ContextProvider<ContextA>;
type ContextBProvider = ContextProvider<ContextB>;
Ok(html! {
<div>
<ContextAProvider context={ContextA(0)}>
<ContextBProvider context={ContextB(1)}>
<ContextAProvider context={ContextA(2)}>
<Test1/>
</ContextAProvider>
<Test2/>
</ContextBProvider>
<Test3/>
</ContextAProvider>
<Test4 />
</div>
})
}
}
type TestComponent = FunctionComponent<TestFunction>;
yew::start_app_in_element::<TestComponent>( yew::start_app_in_element::<TestComponent>(
gloo_utils::document().get_element_by_id("output").unwrap(), gloo_utils::document().get_element_by_id("output").unwrap(),
@ -180,24 +154,19 @@ fn use_context_update_works() {
children: Children, children: Children,
} }
struct RenderCounterFunction; #[function_component]
impl FunctionProvider for RenderCounterFunction { fn RenderCounter(props: &RenderCounterProps) -> Html {
type TProps = RenderCounterProps; let counter = use_mut_ref(|| 0);
*counter.borrow_mut() += 1;
fn run(props: &Self::TProps) -> HtmlResult { html! {
let counter = use_mut_ref(|| 0); <>
*counter.borrow_mut() += 1; <div id={props.id.clone()}>
Ok(html! { { format!("total: {}", counter.borrow()) }
<> </div>
<div id={props.id.clone()}> { props.children.clone() }
{ format!("total: {}", counter.borrow()) } </>
</div>
{ props.children.clone() }
</>
})
} }
} }
type RenderCounter = FunctionComponent<RenderCounterFunction>;
#[derive(Clone, Debug, PartialEq, Properties)] #[derive(Clone, Debug, PartialEq, Properties)]
struct ContextOutletProps { struct ContextOutletProps {
@ -205,75 +174,66 @@ fn use_context_update_works() {
#[prop_or_default] #[prop_or_default]
magic: usize, magic: usize,
} }
struct ContextOutletFunction;
impl FunctionProvider for ContextOutletFunction {
type TProps = ContextOutletProps;
fn run(props: &Self::TProps) -> HtmlResult { #[function_component]
let counter = use_mut_ref(|| 0); fn ContextOutlet(props: &ContextOutletProps) -> Html {
*counter.borrow_mut() += 1; let counter = use_mut_ref(|| 0);
*counter.borrow_mut() += 1;
let ctx = use_context::<Rc<MyContext>>().expect("context not passed down"); let ctx = use_context::<Rc<MyContext>>().expect("context not passed down");
Ok(html! { html! {
<> <>
<div>{ format!("magic: {}\n", props.magic) }</div> <div>{ format!("magic: {}\n", props.magic) }</div>
<div id={props.id.clone()}> <div id={props.id.clone()}>
{ format!("current: {}, total: {}", ctx.0, counter.borrow()) } { format!("current: {}, total: {}", ctx.0, counter.borrow()) }
</div> </div>
</> </>
})
} }
} }
type ContextOutlet = FunctionComponent<ContextOutletFunction>;
struct TestFunction; #[function_component]
impl FunctionProvider for TestFunction { fn TestComponent() -> Html {
type TProps = (); type MyContextProvider = ContextProvider<Rc<MyContext>>;
fn run(_props: &Self::TProps) -> HtmlResult { let ctx = use_state(|| MyContext("hello".into()));
type MyContextProvider = ContextProvider<Rc<MyContext>>; let rendered = use_mut_ref(|| 0);
let ctx = use_state(|| MyContext("hello".into())); // this is used to force an update specific to test-2
let rendered = use_mut_ref(|| 0); let magic_rc = use_state(|| 0);
let magic: usize = *magic_rc;
// this is used to force an update specific to test-2 {
let magic_rc = use_state(|| 0); let ctx = ctx.clone();
let magic: usize = *magic_rc; use_effect(move || {
{ let count = *rendered.borrow();
let ctx = ctx.clone(); match count {
use_effect(move || { 0 => {
let count = *rendered.borrow(); ctx.set(MyContext("world".into()));
match count { *rendered.borrow_mut() += 1;
0 => { }
ctx.set(MyContext("world".into())); 1 => {
*rendered.borrow_mut() += 1; // force test-2 to re-render.
} magic_rc.set(1);
1 => { *rendered.borrow_mut() += 1;
// force test-2 to re-render. }
magic_rc.set(1); 2 => {
*rendered.borrow_mut() += 1; ctx.set(MyContext("hello world!".into()));
} *rendered.borrow_mut() += 1;
2 => { }
ctx.set(MyContext("hello world!".into())); _ => (),
*rendered.borrow_mut() += 1; };
} || {}
_ => (), });
}; }
|| {} html! {
}); <MyContextProvider context={Rc::new((*ctx).clone())}>
} <RenderCounter id="test-0">
Ok(html! { <ContextOutlet id="test-1"/>
<MyContextProvider context={Rc::new((*ctx).clone())}> <ContextOutlet id="test-2" {magic}/>
<RenderCounter id="test-0"> </RenderCounter>
<ContextOutlet id="test-1"/> </MyContextProvider>
<ContextOutlet id="test-2" {magic}/>
</RenderCounter>
</MyContextProvider>
})
} }
} }
type TestComponent = FunctionComponent<TestFunction>;
yew::start_app_in_element::<TestComponent>( yew::start_app_in_element::<TestComponent>(
gloo_utils::document().get_element_by_id("output").unwrap(), gloo_utils::document().get_element_by_id("output").unwrap(),

View File

@ -4,17 +4,12 @@ use common::obtain_result;
use std::ops::{Deref, DerefMut}; use std::ops::{Deref, DerefMut};
use std::rc::Rc; use std::rc::Rc;
use wasm_bindgen_test::*; use wasm_bindgen_test::*;
use yew::functional::{ use yew::prelude::*;
use_effect_with_deps, use_mut_ref, use_state, FunctionComponent, FunctionProvider,
};
use yew::{html, HtmlResult, Properties};
wasm_bindgen_test::wasm_bindgen_test_configure!(run_in_browser); wasm_bindgen_test::wasm_bindgen_test_configure!(run_in_browser);
#[wasm_bindgen_test] #[wasm_bindgen_test]
fn use_effect_destroys_on_component_drop() { fn use_effect_destroys_on_component_drop() {
struct UseEffectFunction {}
struct UseEffectWrapper {}
#[derive(Properties, Clone)] #[derive(Properties, Clone)]
struct WrapperProps { struct WrapperProps {
destroy_called: Rc<dyn Fn()>, destroy_called: Rc<dyn Fn()>,
@ -34,42 +29,37 @@ fn use_effect_destroys_on_component_drop() {
false false
} }
} }
type UseEffectComponent = FunctionComponent<UseEffectFunction>;
type UseEffectWrapperComponent = FunctionComponent<UseEffectWrapper>;
impl FunctionProvider for UseEffectFunction {
type TProps = FunctionProps;
fn run(props: &Self::TProps) -> HtmlResult { #[function_component(UseEffectComponent)]
let effect_called = props.effect_called.clone(); fn use_effect_comp(props: &FunctionProps) -> Html {
let destroy_called = props.destroy_called.clone(); let effect_called = props.effect_called.clone();
use_effect_with_deps( let destroy_called = props.destroy_called.clone();
move |_| { use_effect_with_deps(
effect_called(); move |_| {
#[allow(clippy::redundant_closure)] // Otherwise there is a build error effect_called();
move || destroy_called() #[allow(clippy::redundant_closure)] // Otherwise there is a build error
}, move || destroy_called()
(), },
); (),
Ok(html! {}) );
} html! {}
} }
impl FunctionProvider for UseEffectWrapper {
type TProps = WrapperProps;
fn run(props: &Self::TProps) -> HtmlResult { #[function_component(UseEffectWrapperComponent)]
let show = use_state(|| true); fn use_effect_wrapper_comp(props: &WrapperProps) -> Html {
if *show { let show = use_state(|| true);
let effect_called: Rc<dyn Fn()> = { Rc::new(move || show.set(false)) }; if *show {
Ok(html! { let effect_called: Rc<dyn Fn()> = { Rc::new(move || show.set(false)) };
<UseEffectComponent destroy_called={props.destroy_called.clone()} {effect_called} /> html! {
}) <UseEffectComponent destroy_called={props.destroy_called.clone()} {effect_called} />
} else { }
Ok(html! { } else {
<div>{ "EMPTY" }</div> html! {
}) <div>{ "EMPTY" }</div>
} }
} }
} }
let destroy_counter = Rc::new(std::cell::RefCell::new(0)); let destroy_counter = Rc::new(std::cell::RefCell::new(0));
let destroy_counter_c = destroy_counter.clone(); let destroy_counter_c = destroy_counter.clone();
yew::start_app_with_props_in_element::<UseEffectWrapperComponent>( yew::start_app_with_props_in_element::<UseEffectWrapperComponent>(
@ -83,35 +73,30 @@ fn use_effect_destroys_on_component_drop() {
#[wasm_bindgen_test] #[wasm_bindgen_test]
fn use_effect_works_many_times() { fn use_effect_works_many_times() {
struct UseEffectFunction {} #[function_component(UseEffectComponent)]
impl FunctionProvider for UseEffectFunction { fn use_effect_comp() -> Html {
type TProps = (); let counter = use_state(|| 0);
let counter_clone = counter.clone();
fn run(_: &Self::TProps) -> HtmlResult { use_effect_with_deps(
let counter = use_state(|| 0); move |_| {
let counter_clone = counter.clone(); if *counter_clone < 4 {
counter_clone.set(*counter_clone + 1);
}
|| {}
},
*counter,
);
use_effect_with_deps( html! {
move |_| { <div>
if *counter_clone < 4 { { "The test result is" }
counter_clone.set(*counter_clone + 1); <div id="result">{ *counter }</div>
} { "\n" }
|| {} </div>
},
*counter,
);
Ok(html! {
<div>
{ "The test result is" }
<div id="result">{ *counter }</div>
{ "\n" }
</div>
})
} }
} }
type UseEffectComponent = FunctionComponent<UseEffectFunction>;
yew::start_app_in_element::<UseEffectComponent>( yew::start_app_in_element::<UseEffectComponent>(
gloo_utils::document().get_element_by_id("output").unwrap(), gloo_utils::document().get_element_by_id("output").unwrap(),
); );
@ -121,32 +106,28 @@ fn use_effect_works_many_times() {
#[wasm_bindgen_test] #[wasm_bindgen_test]
fn use_effect_works_once() { fn use_effect_works_once() {
struct UseEffectFunction {} #[function_component(UseEffectComponent)]
impl FunctionProvider for UseEffectFunction { fn use_effect_comp() -> Html {
type TProps = (); let counter = use_state(|| 0);
let counter_clone = counter.clone();
fn run(_: &Self::TProps) -> HtmlResult { use_effect_with_deps(
let counter = use_state(|| 0); move |_| {
let counter_clone = counter.clone(); counter_clone.set(*counter_clone + 1);
|| panic!("Destructor should not have been called")
},
(),
);
use_effect_with_deps( html! {
move |_| { <div>
counter_clone.set(*counter_clone + 1); { "The test result is" }
|| panic!("Destructor should not have been called") <div id="result">{ *counter }</div>
}, { "\n" }
(), </div>
);
Ok(html! {
<div>
{ "The test result is" }
<div id="result">{ *counter }</div>
{ "\n" }
</div>
})
} }
} }
type UseEffectComponent = FunctionComponent<UseEffectFunction>;
yew::start_app_in_element::<UseEffectComponent>( yew::start_app_in_element::<UseEffectComponent>(
gloo_utils::document().get_element_by_id("output").unwrap(), gloo_utils::document().get_element_by_id("output").unwrap(),
); );
@ -156,45 +137,41 @@ fn use_effect_works_once() {
#[wasm_bindgen_test] #[wasm_bindgen_test]
fn use_effect_refires_on_dependency_change() { fn use_effect_refires_on_dependency_change() {
struct UseEffectFunction {} #[function_component(UseEffectComponent)]
impl FunctionProvider for UseEffectFunction { fn use_effect_comp() -> Html {
type TProps = (); let number_ref = use_mut_ref(|| 0);
let number_ref_c = number_ref.clone();
fn run(_: &Self::TProps) -> HtmlResult { let number_ref2 = use_mut_ref(|| 0);
let number_ref = use_mut_ref(|| 0); let number_ref2_c = number_ref2.clone();
let number_ref_c = number_ref.clone(); let arg = *number_ref.borrow_mut().deref_mut();
let number_ref2 = use_mut_ref(|| 0); let counter = use_state(|| 0);
let number_ref2_c = number_ref2.clone(); use_effect_with_deps(
let arg = *number_ref.borrow_mut().deref_mut(); move |dep| {
let counter = use_state(|| 0); let mut ref_mut = number_ref_c.borrow_mut();
use_effect_with_deps( let inner_ref_mut = ref_mut.deref_mut();
move |dep| { if *inner_ref_mut < 1 {
let mut ref_mut = number_ref_c.borrow_mut(); *inner_ref_mut += 1;
let inner_ref_mut = ref_mut.deref_mut(); assert_eq!(dep, &0);
if *inner_ref_mut < 1 { } else {
*inner_ref_mut += 1; assert_eq!(dep, &1);
assert_eq!(dep, &0); }
} else { counter.set(10); // we just need to make sure it does not panic
assert_eq!(dep, &1); move || {
} counter.set(11);
counter.set(10); // we just need to make sure it does not panic *number_ref2_c.borrow_mut().deref_mut() += 1;
move || { }
counter.set(11); },
*number_ref2_c.borrow_mut().deref_mut() += 1; arg,
} );
}, html! {
arg, <div>
); {"The test result is"}
Ok(html! { <div id="result">{*number_ref.borrow_mut().deref_mut()}{*number_ref2.borrow_mut().deref_mut()}</div>
<div> {"\n"}
{"The test result is"} </div>
<div id="result">{*number_ref.borrow_mut().deref_mut()}{*number_ref2.borrow_mut().deref_mut()}</div>
{"\n"}
</div>
})
} }
} }
type UseEffectComponent = FunctionComponent<UseEffectFunction>;
yew::start_app_in_element::<UseEffectComponent>( yew::start_app_in_element::<UseEffectComponent>(
gloo_utils::document().get_element_by_id("output").unwrap(), gloo_utils::document().get_element_by_id("output").unwrap(),
); );

View File

@ -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! {
<div>
{"The test output is: "}
<div id="result">{*memoed_val}</div>
{"\n"}
</div>
}
}
yew::start_app_in_element::<UseMemoComponent>(
gloo_utils::document().get_element_by_id("output").unwrap(),
);
let result = obtain_result();
assert_eq!(result.as_str(), "true");
}

View File

@ -31,30 +31,27 @@ impl Reducible for CounterState {
#[wasm_bindgen_test] #[wasm_bindgen_test]
fn use_reducer_works() { fn use_reducer_works() {
struct UseReducerFunction {} #[function_component(UseReducerComponent)]
impl FunctionProvider for UseReducerFunction { fn use_reducer_comp() -> Html {
type TProps = (); let counter = use_reducer(|| CounterState { counter: 10 });
fn run(_: &Self::TProps) -> HtmlResult {
let counter = use_reducer(|| CounterState { counter: 10 });
let counter_clone = counter.clone(); let counter_clone = counter.clone();
use_effect_with_deps( use_effect_with_deps(
move |_| { move |_| {
counter_clone.dispatch(1); counter_clone.dispatch(1);
|| {} || {}
}, },
(), (),
); );
Ok(html! { html! {
<div> <div>
{"The test result is"} {"The test result is"}
<div id="result">{counter.counter}</div> <div id="result">{counter.counter}</div>
{"\n"} {"\n"}
</div> </div>
})
} }
} }
type UseReducerComponent = FunctionComponent<UseReducerFunction>;
yew::start_app_in_element::<UseReducerComponent>( yew::start_app_in_element::<UseReducerComponent>(
gloo_utils::document().get_element_by_id("output").unwrap(), gloo_utils::document().get_element_by_id("output").unwrap(),
); );
@ -80,42 +77,39 @@ impl Reducible for ContentState {
#[wasm_bindgen_test] #[wasm_bindgen_test]
fn use_reducer_eq_works() { fn use_reducer_eq_works() {
struct UseReducerFunction {} #[function_component(UseReducerComponent)]
impl FunctionProvider for UseReducerFunction { fn use_reducer_comp() -> Html {
type TProps = (); let content = use_reducer_eq(|| ContentState {
fn run(_: &Self::TProps) -> HtmlResult { content: HashSet::default(),
let content = use_reducer_eq(|| ContentState { });
content: HashSet::default(),
});
let render_count = use_mut_ref(|| 0); let render_count = use_mut_ref(|| 0);
let render_count = { let render_count = {
let mut render_count = render_count.borrow_mut(); let mut render_count = render_count.borrow_mut();
*render_count += 1; *render_count += 1;
*render_count *render_count
}; };
let add_content_a = { let add_content_a = {
let content = content.clone(); let content = content.clone();
Callback::from(move |_| content.dispatch("A".to_string())) Callback::from(move |_| content.dispatch("A".to_string()))
}; };
let add_content_b = Callback::from(move |_| content.dispatch("B".to_string())); let add_content_b = Callback::from(move |_| content.dispatch("B".to_string()));
Ok(html! { html! {
<> <>
<div> <div>
{"This component has been rendered: "}<span id="result">{render_count}</span>{" Time(s)."} {"This component has been rendered: "}<span id="result">{render_count}</span>{" Time(s)."}
</div> </div>
<button onclick={add_content_a} id="add-a">{"Add A to Content"}</button> <button onclick={add_content_a} id="add-a">{"Add A to Content"}</button>
<button onclick={add_content_b} id="add-b">{"Add B to Content"}</button> <button onclick={add_content_b} id="add-b">{"Add B to Content"}</button>
</> </>
})
} }
} }
type UseReducerComponent = FunctionComponent<UseReducerFunction>;
yew::start_app_in_element::<UseReducerComponent>( yew::start_app_in_element::<UseReducerComponent>(
document().get_element_by_id("output").unwrap(), document().get_element_by_id("output").unwrap(),
); );

View File

@ -3,34 +3,29 @@ mod common;
use common::obtain_result; use common::obtain_result;
use std::ops::DerefMut; use std::ops::DerefMut;
use wasm_bindgen_test::*; use wasm_bindgen_test::*;
use yew::functional::{use_mut_ref, use_state, FunctionComponent, FunctionProvider}; use yew::prelude::*;
use yew::{html, HtmlResult};
wasm_bindgen_test::wasm_bindgen_test_configure!(run_in_browser); wasm_bindgen_test::wasm_bindgen_test_configure!(run_in_browser);
#[wasm_bindgen_test] #[wasm_bindgen_test]
fn use_ref_works() { fn use_ref_works() {
struct UseRefFunction {} #[function_component(UseRefComponent)]
impl FunctionProvider for UseRefFunction { fn use_ref_comp() -> Html {
type TProps = (); let ref_example = use_mut_ref(|| 0);
*ref_example.borrow_mut().deref_mut() += 1;
fn run(_: &Self::TProps) -> HtmlResult { let counter = use_state(|| 0);
let ref_example = use_mut_ref(|| 0); if *counter < 5 {
*ref_example.borrow_mut().deref_mut() += 1; counter.set(*counter + 1)
let counter = use_state(|| 0); }
if *counter < 5 { html! {
counter.set(*counter + 1) <div>
} {"The test output is: "}
Ok(html! { <div id="result">{*ref_example.borrow_mut().deref_mut() > 4}</div>
<div> {"\n"}
{"The test output is: "} </div>
<div id="result">{*ref_example.borrow_mut().deref_mut() > 4}</div>
{"\n"}
</div>
})
} }
} }
type UseRefComponent = FunctionComponent<UseRefFunction>;
yew::start_app_in_element::<UseRefComponent>( yew::start_app_in_element::<UseRefComponent>(
gloo_utils::document().get_element_by_id("output").unwrap(), gloo_utils::document().get_element_by_id("output").unwrap(),
); );

View File

@ -2,34 +2,27 @@ mod common;
use common::obtain_result; use common::obtain_result;
use wasm_bindgen_test::*; use wasm_bindgen_test::*;
use yew::functional::{ use yew::prelude::*;
use_effect_with_deps, use_state, use_state_eq, FunctionComponent, FunctionProvider,
};
use yew::{html, HtmlResult};
wasm_bindgen_test::wasm_bindgen_test_configure!(run_in_browser); wasm_bindgen_test::wasm_bindgen_test_configure!(run_in_browser);
#[wasm_bindgen_test] #[wasm_bindgen_test]
fn use_state_works() { fn use_state_works() {
struct UseStateFunction {} #[function_component(UseComponent)]
impl FunctionProvider for UseStateFunction { fn use_state_comp() -> Html {
type TProps = (); let counter = use_state(|| 0);
if *counter < 5 {
fn run(_: &Self::TProps) -> HtmlResult { counter.set(*counter + 1)
let counter = use_state(|| 0); }
if *counter < 5 { html! {
counter.set(*counter + 1) <div>
} {"Test Output: "}
return Ok(html! { <div id="result">{*counter}</div>
<div> {"\n"}
{"Test Output: "} </div>
<div id="result">{*counter}</div>
{"\n"}
</div>
});
} }
} }
type UseComponent = FunctionComponent<UseStateFunction>;
yew::start_app_in_element::<UseComponent>( yew::start_app_in_element::<UseComponent>(
gloo_utils::document().get_element_by_id("output").unwrap(), gloo_utils::document().get_element_by_id("output").unwrap(),
); );
@ -39,42 +32,38 @@ fn use_state_works() {
#[wasm_bindgen_test] #[wasm_bindgen_test]
fn multiple_use_state_setters() { fn multiple_use_state_setters() {
struct UseStateFunction {} #[function_component(UseComponent)]
impl FunctionProvider for UseStateFunction { fn use_state_comp() -> Html {
type TProps = (); let counter = use_state(|| 0);
let counter_clone = counter.clone();
fn run(_: &Self::TProps) -> HtmlResult { use_effect_with_deps(
let counter = use_state(|| 0); move |_| {
let counter_clone = counter.clone(); // 1st location
use_effect_with_deps( counter_clone.set(*counter_clone + 1);
move |_| { || {}
// 1st location },
counter_clone.set(*counter_clone + 1); (),
|| {} );
}, let another_scope = {
(), let counter = counter.clone();
); move || {
let another_scope = { if *counter < 11 {
let counter = counter.clone(); // 2nd location
move || { counter.set(*counter + 10)
if *counter < 11 {
// 2nd location
counter.set(*counter + 10)
}
} }
}; }
another_scope(); };
Ok(html! { another_scope();
<div> html! {
{ "Test Output: " } <div>
// expected output { "Test Output: " }
<div id="result">{ *counter }</div> // expected output
{ "\n" } <div id="result">{ *counter }</div>
</div> { "\n" }
}) </div>
} }
} }
type UseComponent = FunctionComponent<UseStateFunction>;
yew::start_app_in_element::<UseComponent>( yew::start_app_in_element::<UseComponent>(
gloo_utils::document().get_element_by_id("output").unwrap(), gloo_utils::document().get_element_by_id("output").unwrap(),
); );
@ -87,26 +76,21 @@ fn use_state_eq_works() {
use std::sync::atomic::{AtomicUsize, Ordering}; use std::sync::atomic::{AtomicUsize, Ordering};
static RENDER_COUNT: AtomicUsize = AtomicUsize::new(0); static RENDER_COUNT: AtomicUsize = AtomicUsize::new(0);
struct UseStateFunction {} #[function_component(UseComponent)]
fn use_state_comp() -> Html {
RENDER_COUNT.fetch_add(1, Ordering::Relaxed);
let counter = use_state_eq(|| 0);
counter.set(1);
impl FunctionProvider for UseStateFunction { html! {
type TProps = (); <div>
{"Test Output: "}
fn run(_: &Self::TProps) -> HtmlResult { <div id="result">{*counter}</div>
RENDER_COUNT.fetch_add(1, Ordering::Relaxed); {"\n"}
let counter = use_state_eq(|| 0); </div>
counter.set(1);
Ok(html! {
<div>
{"Test Output: "}
<div id="result">{*counter}</div>
{"\n"}
</div>
})
} }
} }
type UseComponent = FunctionComponent<UseStateFunction>;
yew::start_app_in_element::<UseComponent>( yew::start_app_in_element::<UseComponent>(
gloo_utils::document().get_element_by_id("output").unwrap(), gloo_utils::document().get_element_by_id("output").unwrap(),
); );

View File

@ -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. 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`. 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. 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 ```rust
use web_sys::{Event, EventTarget}; use web_sys::{Event, EventTarget};
use std::borrow::Cow; use std::borrow::Cow;
use gloo::events::EventListener; use gloo::events::EventListener;
use yew::prelude::*;
#[hook]
pub fn use_event<E, F>(target: &EventTarget, event_type: E, callback: F) pub fn use_event<E, F>(target: &EventTarget, event_type: E, callback: F)
where where
E: Into<Cow<'static, str>>, E: Into<Cow<'static, str>>,
@ -65,6 +68,7 @@ use std::borrow::Cow;
use std::rc::Rc; use std::rc::Rc;
use gloo::events::EventListener; use gloo::events::EventListener;
#[hook]
pub fn use_event<E, F>(target: &EventTarget, event_type: E, callback: F) pub fn use_event<E, F>(target: &EventTarget, event_type: E, callback: F)
where where
E: Into<Cow<'static, str>>, E: Into<Cow<'static, str>>,

View File

@ -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 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. 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 #### Pre-defined Hooks
Yew comes with the following predefined Hooks: Yew comes with the following predefined Hooks:
- [`use_state`](./../function-components/pre-defined-hooks.mdx#use_state) - [`use_state`](./../function-components/pre-defined-hooks.mdx#use_state)
- [`use_state_eq`](./../function-components/pre-defined-hooks.mdx#use_state_eq) - [`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_mut_ref`](./../function-components/pre-defined-hooks.mdx#use_mut_ref)
- [`use_node_ref`](./../function-components/pre-defined-hooks.mdx#use_node_ref) - [`use_node_ref`](./../function-components/pre-defined-hooks.mdx#use_node_ref)
- [`use_reducer`](./../function-components/pre-defined-hooks.mdx#use_reducer) - [`use_reducer`](./../function-components/pre-defined-hooks.mdx#use_reducer)

View File

@ -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`. This hook requires the state object to implement `PartialEq`.
## `use_ref` ## `use_memo`
`use_ref` 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 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. 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 ```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)] #[derive(PartialEq, Properties)]
fn ref_hook() -> Html { pub struct Props {
let message = use_ref(|| "Some Expensive State.".to_string()); 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! { html! {
<div> <div>

View File

@ -60,6 +60,7 @@ struct User {
name: String, name: String,
} }
#[hook]
fn use_user() -> SuspensionResult<User> { fn use_user() -> SuspensionResult<User> {
match load_user() { match load_user() {
// If a user is loaded, then we return it as Ok(user). // If a user is loaded, then we return it as Ok(user).
@ -96,6 +97,7 @@ fn on_load_user_complete<F: FnOnce()>(_fn: F) {
todo!() // implementation omitted. todo!() // implementation omitted.
} }
#[hook]
fn use_user() -> SuspensionResult<User> { fn use_user() -> SuspensionResult<User> {
match load_user() { match load_user() {
// If a user is loaded, then we return it as Ok(user). // If a user is loaded, then we return it as Ok(user).

View File

@ -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 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). 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.