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

View File

@ -57,6 +57,7 @@ impl PartialEq for UuidState {
}
}
#[hook]
fn use_random_uuid() -> SuspensionResult<Uuid> {
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()>> {
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
/// sure captured values (if any) are up to date.
#[hook]
pub fn use_bridge<T, F>(on_output: F) -> UseBridgeHandle<T>
where
T: Bridged,

View File

@ -21,7 +21,9 @@ lazy_static = "1"
proc-macro-error = "1"
proc-macro2 = "1"
quote = "1"
syn = { version = "1", features = ["full", "extra-traits"] }
syn = { version = "1", features = ["full", "extra-traits", "visit-mut"] }
once_cell = "1"
prettyplease = "0.1.1"
# testing
[dev-dependencies]

View File

@ -3,7 +3,11 @@ use quote::{format_ident, quote, ToTokens};
use syn::parse::{Parse, ParseStream};
use syn::punctuated::Punctuated;
use syn::token::{Comma, Fn};
use syn::{Attribute, Block, FnArg, Generics, Ident, Item, ItemFn, ReturnType, Type, Visibility};
use syn::{
visit_mut, Attribute, Block, FnArg, Generics, Ident, Item, ItemFn, ReturnType, Type, Visibility,
};
use crate::hook::BodyRewriter;
#[derive(Clone)]
pub struct FunctionComponent {
@ -169,7 +173,7 @@ fn print_fn(func_comp: FunctionComponent, use_fn_name: bool) -> TokenStream {
fn_token,
name,
attrs,
block,
mut block,
return_type,
generics,
arg,
@ -184,9 +188,14 @@ fn print_fn(func_comp: FunctionComponent, use_fn_name: bool) -> TokenStream {
Ident::new("inner", Span::mixed_site())
};
let ctx_ident = Ident::new("ctx", Span::mixed_site());
let mut body_rewriter = BodyRewriter::default();
visit_mut::visit_block_mut(&mut body_rewriter, &mut *block);
quote! {
#(#attrs)*
#fn_token #name #ty_generics (#arg) -> #return_type
#fn_token #name #ty_generics (#ctx_ident: &mut ::yew::functional::HookContext, #arg) -> #return_type
#where_clause
{
#block
@ -241,6 +250,8 @@ pub fn function_component_impl(
Ident::new("inner", Span::mixed_site())
};
let ctx_ident = Ident::new("ctx", Span::mixed_site());
let quoted = quote! {
#[doc(hidden)]
#[allow(non_camel_case_types)]
@ -253,10 +264,10 @@ pub fn function_component_impl(
impl #impl_generics ::yew::functional::FunctionProvider for #provider_name #ty_generics #where_clause {
type TProps = #props_type;
fn run(#provider_props: &Self::TProps) -> ::yew::html::HtmlResult {
fn run(#ctx_ident: &mut ::yew::functional::HookContext, #provider_props: &Self::TProps) -> ::yew::html::HtmlResult {
#func
::yew::html::IntoHtmlResult::into_html_result(#fn_name #fn_generics (#provider_props))
::yew::html::IntoHtmlResult::into_html_result(#fn_name #fn_generics (#ctx_ident, #provider_props))
}
}

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 derive_props;
mod function_component;
mod hook;
mod html_tree;
mod props;
mod stringify;
use derive_props::DerivePropsInput;
use function_component::{function_component_impl, FunctionComponent, FunctionComponentName};
use hook::{hook_impl, HookFn};
use html_tree::{HtmlRoot, HtmlRootVNode};
use proc_macro::TokenStream;
use quote::ToTokens;
@ -122,11 +124,9 @@ pub fn classes(input: TokenStream) -> TokenStream {
TokenStream::from(classes.into_token_stream())
}
#[proc_macro_error::proc_macro_error]
#[proc_macro_attribute]
pub fn function_component(
attr: proc_macro::TokenStream,
item: proc_macro::TokenStream,
) -> proc_macro::TokenStream {
pub fn function_component(attr: TokenStream, item: TokenStream) -> proc_macro::TokenStream {
let item = parse_macro_input!(item as FunctionComponent);
let attr = parse_macro_input!(attr as FunctionComponentName);
@ -134,3 +134,19 @@ pub fn function_component(
.unwrap_or_else(|err| err.to_compile_error())
.into()
}
#[proc_macro_error::proc_macro_error]
#[proc_macro_attribute]
pub fn hook(attr: TokenStream, item: TokenStream) -> proc_macro::TokenStream {
let item = parse_macro_input!(item as HookFn);
if let Some(m) = proc_macro2::TokenStream::from(attr).into_iter().next() {
return syn::Error::new_spanned(m, "hook attribute does not accept any arguments")
.into_compile_error()
.into();
}
hook_impl(item)
.unwrap_or_else(|err| err.to_compile_error())
.into()
}

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::*;
/// A hook to access the [`Navigator`].
#[hook]
pub fn use_navigator() -> Option<Navigator> {
use_context::<NavigatorContext>().map(|m| m.navigator())
}
/// A hook to access the current [`Location`].
#[hook]
pub fn use_location() -> Option<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
/// `.unwrap()` to unwrap.
#[hook]
pub fn use_route<R>() -> Option<R>
where
R: Routable + 'static,

View File

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

View File

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

View File

@ -1,5 +1,6 @@
use crate::callback::Callback;
use crate::context::ContextHandle;
use crate::functional::{get_current_scope, use_hook};
use crate::functional::{hook, use_component_scope, use_memo, use_state};
/// Hook for consuming context values in function components.
/// The context of the type passed as `T` is returned. If there is no such context in scope, `None` is returned.
@ -28,38 +29,29 @@ use crate::functional::{get_current_scope, use_hook};
/// }
/// }
/// ```
#[hook]
pub fn use_context<T: Clone + PartialEq + 'static>() -> Option<T> {
struct UseContextState<T2: Clone + PartialEq + 'static> {
initialized: bool,
context: Option<(T2, ContextHandle<T2>)>,
struct UseContext<T: Clone + PartialEq + 'static> {
context: Option<(T, ContextHandle<T>)>,
}
let scope = get_current_scope()
.expect("No current Scope. `use_context` can only be called inside function components");
let scope = use_component_scope();
use_hook(
move || UseContextState {
initialized: false,
context: None,
},
|state: &mut UseContextState<T>, updater| {
if !state.initialized {
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())
},
|state| {
state.context = None;
let val = use_state(|| -> Option<T> { None });
let state = {
let val_dispatcher = val.setter();
use_memo(
move |_| UseContext {
context: scope.context::<T>(Callback::from(move |m| {
val_dispatcher.clone().set(Some(m));
})),
},
(),
)
};
// we fallback to initial value if it was not updated.
(*val)
.clone()
.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::rc::Rc;
use std::cell::RefCell;
struct UseEffect<Destructor> {
runner: Option<Box<dyn FnOnce() -> Destructor>>,
destructor: Option<Box<Destructor>>,
use crate::functional::{hook, Effect, Hook, HookContext};
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.
@ -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
Destructor: FnOnce() + 'static,
F: FnOnce() -> D + 'static,
D: FnOnce() + 'static,
{
use_hook(
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>>,
use_effect_base(|_| f(), (), |_, _| true);
}
/// 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.
/// To detect changes, dependencies must implement `PartialEq`.
/// 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
Callback: FnOnce(&Dependents) -> Destructor + 'static,
Destructor: FnOnce() + 'static,
Dependents: PartialEq + 'static,
T: PartialEq + 'static,
F: FnOnce(&T) -> D + 'static,
D: FnOnce() + 'static,
{
let deps = Rc::new(deps);
use_hook(
move || {
let destructor: Option<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()
}
},
);
use_effect_base(f, deps, |lhs, rhs| lhs != rhs)
}

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::fmt;
use std::marker::PhantomData;
use std::ops::Deref;
use std::rc::Rc;
use crate::functional::use_hook;
use crate::functional::{hook, Hook, HookContext};
type DispatchFn<T> = Rc<dyn Fn(<T as Reducible>::Action)>;
@ -20,10 +21,9 @@ struct UseReducer<T>
where
T: Reducible,
{
current_state: Rc<T>,
current_state: Rc<RefCell<Rc<T>>>,
// To be replaced with OnceCell once it becomes available in std.
dispatch: RefCell<Option<DispatchFn<T>>>,
dispatch: DispatchFn<T>,
}
/// State handle for [`use_reducer`] and [`use_reducer_eq`] hook
@ -144,51 +144,76 @@ where
}
/// The base function of [`use_reducer`] and [`use_reducer_eq`]
fn use_reducer_base<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
T: Reducible + 'static,
F: FnOnce() -> T,
R: (Fn(&T, &T) -> bool) + 'static,
{
use_hook(
move || UseReducer {
current_state: Rc::new(initial_fn()),
dispatch: RefCell::default(),
},
|s, updater| {
let mut dispatch_ref = s.dispatch.borrow_mut();
struct HookProvider<'hook, T, F>
where
T: Reducible + 'static,
F: 'hook + FnOnce() -> T,
{
_marker: PhantomData<&'hook ()>,
// Create dispatch once.
let dispatch = match *dispatch_ref {
Some(ref m) => (*m).to_owned(),
None => {
init_fn: F,
should_render_fn: fn(&T, &T) -> bool,
}
impl<'hook, T, F> Hook for HookProvider<'hook, T, F>
where
T: Reducible + 'static,
F: 'hook + FnOnce() -> T,
{
type Output = UseReducerHandle<T>;
fn run(self, ctx: &mut HookContext) -> Self::Output {
let Self {
init_fn,
should_render_fn,
..
} = self;
let state = ctx.next_state(move |re_render| {
let val = Rc::new(RefCell::new(Rc::new(init_fn())));
let should_render_fn = Rc::new(should_render_fn);
let dispatch: Rc<dyn Fn(T::Action)> = Rc::new(move |action: T::Action| {
UseReducer {
current_state: val.clone(),
dispatch: Rc::new(move |action: T::Action| {
let should_render = {
let should_render_fn = should_render_fn.clone();
updater.callback(move |state: &mut UseReducer<T>| {
let next_state = state.current_state.clone().reduce(action);
let should_render = should_render_fn(&next_state, &state.current_state);
state.current_state = next_state;
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
});
});
*dispatch_ref = Some(dispatch.clone());
dispatch
}
};
UseReducerHandle {
value: Rc::clone(&s.current_state),
dispatch,
// Currently, this triggers a render immediately, so we need to release the
// borrowed reference first.
if should_render {
re_render()
}
}),
}
});
let value = state.current_state.borrow().clone();
let dispatch = state.dispatch.clone();
UseReducerHandle { value, dispatch }
}
}
HookProvider {
_marker: PhantomData,
init_fn,
should_render_fn,
}
},
|_| {},
)
}
/// This hook is an alternative to [`use_state`](super::use_state()).
@ -259,22 +284,24 @@ where
/// }
/// }
/// ```
pub fn use_reducer<T, F>(initial_fn: F) -> UseReducerHandle<T>
#[hook]
pub fn use_reducer<T, F>(init_fn: F) -> UseReducerHandle<T>
where
T: Reducible + 'static,
F: FnOnce() -> T,
{
use_reducer_base(initial_fn, |_, _| true)
use_reducer_base(init_fn, |_, _| true)
}
/// [`use_reducer`] but only re-renders when `prev_state != next_state`.
///
/// This requires the state to implement [`PartialEq`] in addition to the [`Reducible`] trait
/// required by [`use_reducer`].
pub fn use_reducer_eq<T, F>(initial_fn: F) -> UseReducerHandle<T>
#[hook]
pub fn use_reducer_eq<T, F>(init_fn: F) -> UseReducerHandle<T>
where
T: Reducible + PartialEq + 'static,
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, rc::Rc};
use std::cell::RefCell;
use std::rc::Rc;
use crate::functional::{hook, use_memo, use_state};
use crate::NodeRef;
/// This hook is used for obtaining a mutable reference to a stateful value.
/// Its state persists across renders.
@ -47,25 +50,12 @@ use std::{cell::RefCell, rc::Rc};
/// }
/// }
/// ```
pub fn use_mut_ref<T: 'static>(initial_value: impl FnOnce() -> T) -> Rc<RefCell<T>> {
use_hook(
|| Rc::new(RefCell::new(initial_value())),
|state, _| state.clone(),
|_| {},
)
}
/// This hook is used for obtaining a immutable reference to a stateful value.
/// Its state persists across renders.
///
/// If you need a mutable reference, consider using [`use_mut_ref`](super::use_mut_ref).
/// If you need the component to be re-rendered on state change, consider using [`use_state`](super::use_state()).
pub fn use_ref<T: 'static>(initial_value: impl FnOnce() -> T) -> Rc<T> {
use_hook(
|| Rc::new(initial_value()),
|state, _| Rc::clone(state),
|_| {},
)
#[hook]
pub fn use_mut_ref<T: 'static, F>(init_fn: F) -> Rc<RefCell<T>>
where
F: FnOnce() -> T,
{
use_memo(|_| RefCell::new(init_fn()), ())
}
/// 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 {
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 super::{use_reducer, use_reducer_eq, Reducible, UseReducerDispatcher, UseReducerHandle};
use crate::functional::hook;
struct UseStateReducer<T> {
value: Rc<T>,
value: T,
}
impl<T> Reducible for UseStateReducer<T> {
type Action = T;
fn reduce(self: Rc<Self>, action: Self::Action) -> Rc<Self> {
Rc::new(Self {
value: action.into(),
})
Rc::new(Self { value: action })
}
}
@ -53,14 +52,13 @@ where
/// }
/// }
/// ```
#[hook]
pub fn use_state<T, F>(init_fn: F) -> UseStateHandle<T>
where
T: 'static,
F: FnOnce() -> T,
{
let handle = use_reducer(move || UseStateReducer {
value: Rc::new(init_fn()),
});
let handle = use_reducer(move || UseStateReducer { value: init_fn() });
UseStateHandle { inner: handle }
}
@ -68,14 +66,13 @@ where
/// [`use_state`] but only re-renders when `prev_state != next_state`.
///
/// This hook requires the state to implement [`PartialEq`].
#[hook]
pub fn use_state_eq<T, F>(init_fn: F) -> UseStateHandle<T>
where
T: PartialEq + 'static,
F: FnOnce() -> T,
{
let handle = use_reducer_eq(move || UseStateReducer {
value: Rc::new(init_fn()),
});
let handle = use_reducer_eq(move || UseStateReducer { value: init_fn() });
UseStateHandle { inner: handle }
}

View File

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

View File

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

View File

@ -44,6 +44,7 @@ async fn suspense_works() {
}
}
#[hook]
pub fn use_sleep() -> SuspensionResult<Rc<dyn Fn()>> {
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()>> {
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()>> {
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()>> {
let sleep_state = use_reducer(SleepState::new);

View File

@ -3,10 +3,7 @@ mod common;
use common::obtain_result_by_id;
use std::rc::Rc;
use wasm_bindgen_test::*;
use yew::functional::{
use_context, use_effect, use_mut_ref, use_state, FunctionComponent, FunctionProvider,
};
use yew::{html, Children, ContextProvider, HtmlResult, Properties};
use yew::prelude::*;
wasm_bindgen_test::wasm_bindgen_test_configure!(run_in_browser);
@ -14,33 +11,26 @@ wasm_bindgen_test::wasm_bindgen_test_configure!(run_in_browser);
fn use_context_scoping_works() {
#[derive(Clone, Debug, PartialEq)]
struct ExampleContext(String);
struct UseContextFunctionOuter {}
struct UseContextFunctionInner {}
struct ExpectNoContextFunction {}
type UseContextComponent = FunctionComponent<UseContextFunctionOuter>;
type UseContextComponentInner = FunctionComponent<UseContextFunctionInner>;
type ExpectNoContextComponent = FunctionComponent<ExpectNoContextFunction>;
impl FunctionProvider for ExpectNoContextFunction {
type TProps = ();
fn run(_props: &Self::TProps) -> HtmlResult {
if use_context::<ExampleContext>().is_some() {
#[function_component]
fn ExpectNoContextComponent() -> Html {
let example_context = use_context::<ExampleContext>();
if example_context.is_some() {
console_log!(
"Context should be None here, but was {:?}!",
use_context::<ExampleContext>().unwrap()
example_context
);
};
Ok(html! {
html! {
<div></div>
})
}
}
impl FunctionProvider for UseContextFunctionOuter {
type TProps = ();
fn run(_props: &Self::TProps) -> HtmlResult {
#[function_component]
fn UseContextComponent() -> Html {
type ExampleContextProvider = ContextProvider<ExampleContext>;
Ok(html! {
html! {
<div>
<ExampleContextProvider context={ExampleContext("wrong1".into())}>
<div>{"ignored"}</div>
@ -58,17 +48,14 @@ fn use_context_scoping_works() {
</ExampleContextProvider>
<ExpectNoContextComponent />
</div>
})
}
}
impl FunctionProvider for UseContextFunctionInner {
type TProps = ();
fn run(_props: &Self::TProps) -> HtmlResult {
#[function_component]
fn UseContextComponentInner() -> Html {
let context = use_context::<ExampleContext>();
Ok(html! {
html! {
<div id="result">{ &context.unwrap().0 }</div>
})
}
}
@ -86,67 +73,56 @@ fn use_context_works_with_multiple_types() {
#[derive(Clone, Debug, PartialEq)]
struct ContextB(u32);
struct Test1Function;
impl FunctionProvider for Test1Function {
type TProps = ();
#[function_component]
fn Test1() -> Html {
let ctx_a = use_context::<ContextA>();
let ctx_b = use_context::<ContextB>();
fn run(_props: &Self::TProps) -> HtmlResult {
assert_eq!(use_context::<ContextA>(), Some(ContextA(2)));
assert_eq!(use_context::<ContextB>(), Some(ContextB(1)));
assert_eq!(ctx_a, Some(ContextA(2)));
assert_eq!(ctx_b, 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! {}
}
type Test1 = FunctionComponent<Test1Function>;
struct Test2Function;
impl FunctionProvider for Test2Function {
type TProps = ();
#[function_component]
fn Test3() -> Html {
let ctx_a = use_context::<ContextA>();
let ctx_b = use_context::<ContextB>();
fn run(_props: &Self::TProps) -> HtmlResult {
assert_eq!(use_context::<ContextA>(), Some(ContextA(0)));
assert_eq!(use_context::<ContextB>(), Some(ContextB(1)));
assert_eq!(ctx_a, Some(ContextA(0)));
assert_eq!(ctx_b, None);
Ok(html! {})
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! {}
}
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 {
#[function_component]
fn TestComponent() -> Html {
type ContextAProvider = ContextProvider<ContextA>;
type ContextBProvider = ContextProvider<ContextB>;
Ok(html! {
html! {
<div>
<ContextAProvider context={ContextA(0)}>
<ContextBProvider context={ContextB(1)}>
@ -159,10 +135,8 @@ fn use_context_works_with_multiple_types() {
</ContextAProvider>
<Test4 />
</div>
})
}
}
type TestComponent = FunctionComponent<TestFunction>;
yew::start_app_in_element::<TestComponent>(
gloo_utils::document().get_element_by_id("output").unwrap(),
@ -180,24 +154,19 @@ fn use_context_update_works() {
children: Children,
}
struct RenderCounterFunction;
impl FunctionProvider for RenderCounterFunction {
type TProps = RenderCounterProps;
fn run(props: &Self::TProps) -> HtmlResult {
#[function_component]
fn RenderCounter(props: &RenderCounterProps) -> Html {
let counter = use_mut_ref(|| 0);
*counter.borrow_mut() += 1;
Ok(html! {
html! {
<>
<div id={props.id.clone()}>
{ format!("total: {}", counter.borrow()) }
</div>
{ props.children.clone() }
</>
})
}
}
type RenderCounter = FunctionComponent<RenderCounterFunction>;
#[derive(Clone, Debug, PartialEq, Properties)]
struct ContextOutletProps {
@ -205,33 +174,26 @@ fn use_context_update_works() {
#[prop_or_default]
magic: usize,
}
struct ContextOutletFunction;
impl FunctionProvider for ContextOutletFunction {
type TProps = ContextOutletProps;
fn run(props: &Self::TProps) -> HtmlResult {
#[function_component]
fn ContextOutlet(props: &ContextOutletProps) -> Html {
let counter = use_mut_ref(|| 0);
*counter.borrow_mut() += 1;
let ctx = use_context::<Rc<MyContext>>().expect("context not passed down");
Ok(html! {
html! {
<>
<div>{ format!("magic: {}\n", props.magic) }</div>
<div id={props.id.clone()}>
{ format!("current: {}, total: {}", ctx.0, counter.borrow()) }
</div>
</>
})
}
}
type ContextOutlet = FunctionComponent<ContextOutletFunction>;
struct TestFunction;
impl FunctionProvider for TestFunction {
type TProps = ();
fn run(_props: &Self::TProps) -> HtmlResult {
#[function_component]
fn TestComponent() -> Html {
type MyContextProvider = ContextProvider<Rc<MyContext>>;
let ctx = use_state(|| MyContext("hello".into()));
@ -263,17 +225,15 @@ fn use_context_update_works() {
|| {}
});
}
Ok(html! {
html! {
<MyContextProvider context={Rc::new((*ctx).clone())}>
<RenderCounter id="test-0">
<ContextOutlet id="test-1"/>
<ContextOutlet id="test-2" {magic}/>
</RenderCounter>
</MyContextProvider>
})
}
}
type TestComponent = FunctionComponent<TestFunction>;
yew::start_app_in_element::<TestComponent>(
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::rc::Rc;
use wasm_bindgen_test::*;
use yew::functional::{
use_effect_with_deps, use_mut_ref, use_state, FunctionComponent, FunctionProvider,
};
use yew::{html, HtmlResult, Properties};
use yew::prelude::*;
wasm_bindgen_test::wasm_bindgen_test_configure!(run_in_browser);
#[wasm_bindgen_test]
fn use_effect_destroys_on_component_drop() {
struct UseEffectFunction {}
struct UseEffectWrapper {}
#[derive(Properties, Clone)]
struct WrapperProps {
destroy_called: Rc<dyn Fn()>,
@ -34,12 +29,9 @@ fn use_effect_destroys_on_component_drop() {
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)]
fn use_effect_comp(props: &FunctionProps) -> Html {
let effect_called = props.effect_called.clone();
let destroy_called = props.destroy_called.clone();
use_effect_with_deps(
@ -50,26 +42,24 @@ fn use_effect_destroys_on_component_drop() {
},
(),
);
Ok(html! {})
html! {}
}
}
impl FunctionProvider for UseEffectWrapper {
type TProps = WrapperProps;
fn run(props: &Self::TProps) -> HtmlResult {
#[function_component(UseEffectWrapperComponent)]
fn use_effect_wrapper_comp(props: &WrapperProps) -> Html {
let show = use_state(|| true);
if *show {
let effect_called: Rc<dyn Fn()> = { Rc::new(move || show.set(false)) };
Ok(html! {
html! {
<UseEffectComponent destroy_called={props.destroy_called.clone()} {effect_called} />
})
}
} else {
Ok(html! {
html! {
<div>{ "EMPTY" }</div>
})
}
}
}
let destroy_counter = Rc::new(std::cell::RefCell::new(0));
let destroy_counter_c = destroy_counter.clone();
yew::start_app_with_props_in_element::<UseEffectWrapperComponent>(
@ -83,11 +73,8 @@ fn use_effect_destroys_on_component_drop() {
#[wasm_bindgen_test]
fn use_effect_works_many_times() {
struct UseEffectFunction {}
impl FunctionProvider for UseEffectFunction {
type TProps = ();
fn run(_: &Self::TProps) -> HtmlResult {
#[function_component(UseEffectComponent)]
fn use_effect_comp() -> Html {
let counter = use_state(|| 0);
let counter_clone = counter.clone();
@ -101,17 +88,15 @@ fn use_effect_works_many_times() {
*counter,
);
Ok(html! {
html! {
<div>
{ "The test result is" }
<div id="result">{ *counter }</div>
{ "\n" }
</div>
})
}
}
type UseEffectComponent = FunctionComponent<UseEffectFunction>;
yew::start_app_in_element::<UseEffectComponent>(
gloo_utils::document().get_element_by_id("output").unwrap(),
);
@ -121,11 +106,8 @@ fn use_effect_works_many_times() {
#[wasm_bindgen_test]
fn use_effect_works_once() {
struct UseEffectFunction {}
impl FunctionProvider for UseEffectFunction {
type TProps = ();
fn run(_: &Self::TProps) -> HtmlResult {
#[function_component(UseEffectComponent)]
fn use_effect_comp() -> Html {
let counter = use_state(|| 0);
let counter_clone = counter.clone();
@ -137,16 +119,15 @@ fn use_effect_works_once() {
(),
);
Ok(html! {
html! {
<div>
{ "The test result is" }
<div id="result">{ *counter }</div>
{ "\n" }
</div>
})
}
}
type UseEffectComponent = FunctionComponent<UseEffectFunction>;
yew::start_app_in_element::<UseEffectComponent>(
gloo_utils::document().get_element_by_id("output").unwrap(),
);
@ -156,11 +137,8 @@ fn use_effect_works_once() {
#[wasm_bindgen_test]
fn use_effect_refires_on_dependency_change() {
struct UseEffectFunction {}
impl FunctionProvider for UseEffectFunction {
type TProps = ();
fn run(_: &Self::TProps) -> HtmlResult {
#[function_component(UseEffectComponent)]
fn use_effect_comp() -> Html {
let number_ref = use_mut_ref(|| 0);
let number_ref_c = number_ref.clone();
let number_ref2 = use_mut_ref(|| 0);
@ -185,16 +163,15 @@ fn use_effect_refires_on_dependency_change() {
},
arg,
);
Ok(html! {
html! {
<div>
{"The test result is"}
<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>(
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,10 +31,8 @@ impl Reducible for CounterState {
#[wasm_bindgen_test]
fn use_reducer_works() {
struct UseReducerFunction {}
impl FunctionProvider for UseReducerFunction {
type TProps = ();
fn run(_: &Self::TProps) -> HtmlResult {
#[function_component(UseReducerComponent)]
fn use_reducer_comp() -> Html {
let counter = use_reducer(|| CounterState { counter: 10 });
let counter_clone = counter.clone();
@ -45,16 +43,15 @@ fn use_reducer_works() {
},
(),
);
Ok(html! {
html! {
<div>
{"The test result is"}
<div id="result">{counter.counter}</div>
{"\n"}
</div>
})
}
}
type UseReducerComponent = FunctionComponent<UseReducerFunction>;
yew::start_app_in_element::<UseReducerComponent>(
gloo_utils::document().get_element_by_id("output").unwrap(),
);
@ -80,10 +77,8 @@ impl Reducible for ContentState {
#[wasm_bindgen_test]
fn use_reducer_eq_works() {
struct UseReducerFunction {}
impl FunctionProvider for UseReducerFunction {
type TProps = ();
fn run(_: &Self::TProps) -> HtmlResult {
#[function_component(UseReducerComponent)]
fn use_reducer_comp() -> Html {
let content = use_reducer_eq(|| ContentState {
content: HashSet::default(),
});
@ -104,7 +99,7 @@ fn use_reducer_eq_works() {
let add_content_b = Callback::from(move |_| content.dispatch("B".to_string()));
Ok(html! {
html! {
<>
<div>
{"This component has been rendered: "}<span id="result">{render_count}</span>{" Time(s)."}
@ -112,10 +107,9 @@ fn use_reducer_eq_works() {
<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>
</>
})
}
}
type UseReducerComponent = FunctionComponent<UseReducerFunction>;
yew::start_app_in_element::<UseReducerComponent>(
document().get_element_by_id("output").unwrap(),
);

View File

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

View File

@ -2,34 +2,27 @@ mod common;
use common::obtain_result;
use wasm_bindgen_test::*;
use yew::functional::{
use_effect_with_deps, use_state, use_state_eq, FunctionComponent, FunctionProvider,
};
use yew::{html, HtmlResult};
use yew::prelude::*;
wasm_bindgen_test::wasm_bindgen_test_configure!(run_in_browser);
#[wasm_bindgen_test]
fn use_state_works() {
struct UseStateFunction {}
impl FunctionProvider for UseStateFunction {
type TProps = ();
fn run(_: &Self::TProps) -> HtmlResult {
#[function_component(UseComponent)]
fn use_state_comp() -> Html {
let counter = use_state(|| 0);
if *counter < 5 {
counter.set(*counter + 1)
}
return Ok(html! {
html! {
<div>
{"Test Output: "}
<div id="result">{*counter}</div>
{"\n"}
</div>
});
}
}
type UseComponent = FunctionComponent<UseStateFunction>;
yew::start_app_in_element::<UseComponent>(
gloo_utils::document().get_element_by_id("output").unwrap(),
);
@ -39,11 +32,8 @@ fn use_state_works() {
#[wasm_bindgen_test]
fn multiple_use_state_setters() {
struct UseStateFunction {}
impl FunctionProvider for UseStateFunction {
type TProps = ();
fn run(_: &Self::TProps) -> HtmlResult {
#[function_component(UseComponent)]
fn use_state_comp() -> Html {
let counter = use_state(|| 0);
let counter_clone = counter.clone();
use_effect_with_deps(
@ -64,17 +54,16 @@ fn multiple_use_state_setters() {
}
};
another_scope();
Ok(html! {
html! {
<div>
{ "Test Output: " }
// expected output
<div id="result">{ *counter }</div>
{ "\n" }
</div>
})
}
}
type UseComponent = FunctionComponent<UseStateFunction>;
yew::start_app_in_element::<UseComponent>(
gloo_utils::document().get_element_by_id("output").unwrap(),
);
@ -87,26 +76,21 @@ fn use_state_eq_works() {
use std::sync::atomic::{AtomicUsize, Ordering};
static RENDER_COUNT: AtomicUsize = AtomicUsize::new(0);
struct UseStateFunction {}
impl FunctionProvider for UseStateFunction {
type TProps = ();
fn run(_: &Self::TProps) -> HtmlResult {
#[function_component(UseComponent)]
fn use_state_comp() -> Html {
RENDER_COUNT.fetch_add(1, Ordering::Relaxed);
let counter = use_state_eq(|| 0);
counter.set(1);
Ok(html! {
html! {
<div>
{"Test Output: "}
<div id="result">{*counter}</div>
{"\n"}
</div>
})
}
}
type UseComponent = FunctionComponent<UseStateFunction>;
yew::start_app_in_element::<UseComponent>(
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.
We'll start by creating a new function called `use_event`.
The `use_` prefix conventionally denotes that a function is a hook.
The `use_` prefix denotes that a function is a hook.
This function will take an event target, a event type and a callback.
All hooks must be marked by `#[hook]` to function as as hook.
```rust
use web_sys::{Event, EventTarget};
use std::borrow::Cow;
use gloo::events::EventListener;
use yew::prelude::*;
#[hook]
pub fn use_event<E, F>(target: &EventTarget, event_type: E, callback: F)
where
E: Into<Cow<'static, str>>,
@ -65,6 +68,7 @@ use std::borrow::Cow;
use std::rc::Rc;
use gloo::events::EventListener;
#[hook]
pub fn use_event<E, F>(target: &EventTarget, event_type: E, callback: F)
where
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
actions. Yew comes with a few pre-defined Hooks. You can also create your own.
Hooks can only be used at the following locations:
- Top level of a function / hook.
- If condition inside a function / hook, given it's not already branched.
- Match condition inside a function / hook, given it's not already branched.
- Blocks inside a function / hook, given it's not already branched.
#### Pre-defined Hooks
Yew comes with the following predefined Hooks:
- [`use_state`](./../function-components/pre-defined-hooks.mdx#use_state)
- [`use_state_eq`](./../function-components/pre-defined-hooks.mdx#use_state_eq)
- [`use_ref`](./../function-components/pre-defined-hooks.mdx#use_ref)
- [`use_memo`](./../function-components/pre-defined-hooks.mdx#use_memo)
- [`use_mut_ref`](./../function-components/pre-defined-hooks.mdx#use_mut_ref)
- [`use_node_ref`](./../function-components/pre-defined-hooks.mdx#use_node_ref)
- [`use_reducer`](./../function-components/pre-defined-hooks.mdx#use_reducer)

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`.
## `use_ref`
`use_ref` is used for obtaining an immutable reference to a value.
## `use_memo`
`use_memo` is used for obtaining an immutable reference to a memoized value.
Its state persists across renders.
Its value will be recalculated only if any of the dependencies values change.
`use_ref` can be useful for keeping things in scope for the lifetime of the component, so long as
`use_memo` can be useful for keeping things in scope for the lifetime of the component, so long as
you don't store a clone of the resulting `Rc` anywhere that outlives the component.
If you need a mutable reference, consider using [`use_mut_ref`](#use_mut_ref).
If you need the component to be re-rendered on state change, consider using [`use_state`](#use_state).
```rust
use yew::{function_component, html, use_ref, use_state, Callback, Html};
use yew::{function_component, html, use_memo, use_state, Callback, Html, Properties};
#[function_component(UseRef)]
fn ref_hook() -> Html {
let message = use_ref(|| "Some Expensive State.".to_string());
#[derive(PartialEq, Properties)]
pub struct Props {
pub step: usize,
}
#[function_component(UseMemo)]
fn ref_hook(props: &Props) -> Html {
// Will only get recalculated if `props.step` value changes
let message = use_memo(
|step| format!("{}. Do Some Expensive Calculation", step),
props.step
);
html! {
<div>

View File

@ -60,6 +60,7 @@ struct User {
name: String,
}
#[hook]
fn use_user() -> SuspensionResult<User> {
match load_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.
}
#[hook]
fn use_user() -> SuspensionResult<User> {
match load_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
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.