mirror of
https://github.com/yewstack/yew.git
synced 2025-12-08 21:26:25 +00:00
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:
parent
22f3f46dd7
commit
485a1b8c4a
@ -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);
|
||||
|
||||
|
||||
@ -57,6 +57,7 @@ impl PartialEq for UuidState {
|
||||
}
|
||||
}
|
||||
|
||||
#[hook]
|
||||
fn use_random_uuid() -> SuspensionResult<Uuid> {
|
||||
let s = use_state(UuidState::new);
|
||||
|
||||
|
||||
@ -28,6 +28,7 @@ impl Reducible for SleepState {
|
||||
}
|
||||
}
|
||||
|
||||
#[hook]
|
||||
pub fn use_sleep() -> SuspensionResult<Rc<dyn Fn()>> {
|
||||
let sleep_state = use_reducer(SleepState::new);
|
||||
|
||||
|
||||
@ -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,
|
||||
|
||||
@ -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]
|
||||
|
||||
@ -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))
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
123
packages/yew-macro/src/hook/body.rs
Normal file
123
packages/yew-macro/src/hook/body.rs
Normal 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.
|
||||
}
|
||||
}
|
||||
121
packages/yew-macro/src/hook/lifetime.rs
Normal file
121
packages/yew-macro/src/hook/lifetime.rs
Normal 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);
|
||||
}
|
||||
}
|
||||
192
packages/yew-macro/src/hook/mod.rs
Normal file
192
packages/yew-macro/src/hook/mod.rs
Normal 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)
|
||||
}
|
||||
183
packages/yew-macro/src/hook/signature.rs
Normal file
183
packages/yew-macro/src/hook/signature.rs
Normal 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()
|
||||
}
|
||||
}
|
||||
@ -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()
|
||||
}
|
||||
|
||||
@ -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() {}
|
||||
@ -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();
|
||||
| ^^^^^^^^^^^
|
||||
@ -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() {}
|
||||
@ -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,
|
||||
|
||||
@ -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 }
|
||||
|
||||
|
||||
@ -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;
|
||||
|
||||
// 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());
|
||||
}));
|
||||
}
|
||||
|
||||
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())
|
||||
/// Runs the hook inside current state, returns output upon completion.
|
||||
fn run(self, ctx: &mut HookContext) -> Self::Output;
|
||||
}
|
||||
|
||||
/// The blanket implementation of boxed hooks.
|
||||
#[doc(hidden)]
|
||||
#[allow(missing_debug_implementations, missing_docs)]
|
||||
pub struct BoxedHook<'hook, T> {
|
||||
inner: Box<dyn 'hook + FnOnce(&mut HookContext) -> T>,
|
||||
}
|
||||
|
||||
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()
|
||||
}
|
||||
}
|
||||
|
||||
HookProvider {}
|
||||
}
|
||||
|
||||
@ -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()))
|
||||
}
|
||||
|
||||
@ -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)
|
||||
}
|
||||
|
||||
37
packages/yew/src/functional/hooks/use_memo.rs
Normal file
37
packages/yew/src/functional/hooks/use_memo.rs
Normal 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
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -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)
|
||||
}
|
||||
|
||||
@ -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()
|
||||
}
|
||||
|
||||
@ -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 }
|
||||
}
|
||||
|
||||
@ -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,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@ -206,6 +206,7 @@ mod ssr_tests {
|
||||
}
|
||||
}
|
||||
|
||||
#[hook]
|
||||
pub fn use_sleep() -> SuspensionResult<Rc<dyn Fn()>> {
|
||||
let sleep_state = use_reducer(SleepState::new);
|
||||
|
||||
|
||||
@ -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 {
|
||||
|
||||
@ -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);
|
||||
|
||||
|
||||
@ -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(),
|
||||
|
||||
@ -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(),
|
||||
);
|
||||
|
||||
53
packages/yew/tests/use_memo.rs
Normal file
53
packages/yew/tests/use_memo.rs
Normal 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");
|
||||
}
|
||||
@ -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(),
|
||||
);
|
||||
|
||||
@ -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(),
|
||||
);
|
||||
|
||||
@ -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(),
|
||||
);
|
||||
|
||||
@ -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>>,
|
||||
|
||||
@ -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)
|
||||
|
||||
@ -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>
|
||||
|
||||
@ -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).
|
||||
|
||||
@ -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.
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user