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::ops::Deref;
|
||||||
use std::rc::Rc;
|
use std::rc::Rc;
|
||||||
use yew::{use_state_eq, UseStateHandle};
|
use yew::prelude::*;
|
||||||
|
|
||||||
#[derive(Clone)]
|
#[derive(Clone)]
|
||||||
pub struct UseBoolToggleHandle {
|
pub struct UseBoolToggleHandle {
|
||||||
@ -47,6 +47,7 @@ impl Deref for UseBoolToggleHandle {
|
|||||||
/// <button {onclick}>{ "Click me" }</button>
|
/// <button {onclick}>{ "Click me" }</button>
|
||||||
/// ...
|
/// ...
|
||||||
/// ```
|
/// ```
|
||||||
|
#[hook]
|
||||||
pub fn use_bool_toggle(default: bool) -> UseBoolToggleHandle {
|
pub fn use_bool_toggle(default: bool) -> UseBoolToggleHandle {
|
||||||
let state = use_state_eq(|| default);
|
let state = use_state_eq(|| default);
|
||||||
|
|
||||||
|
|||||||
@ -57,6 +57,7 @@ impl PartialEq for UuidState {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[hook]
|
||||||
fn use_random_uuid() -> SuspensionResult<Uuid> {
|
fn use_random_uuid() -> SuspensionResult<Uuid> {
|
||||||
let s = use_state(UuidState::new);
|
let s = use_state(UuidState::new);
|
||||||
|
|
||||||
|
|||||||
@ -28,6 +28,7 @@ impl Reducible for SleepState {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[hook]
|
||||||
pub fn use_sleep() -> SuspensionResult<Rc<dyn Fn()>> {
|
pub fn use_sleep() -> SuspensionResult<Rc<dyn Fn()>> {
|
||||||
let sleep_state = use_reducer(SleepState::new);
|
let sleep_state = use_reducer(SleepState::new);
|
||||||
|
|
||||||
|
|||||||
@ -29,6 +29,7 @@ where
|
|||||||
///
|
///
|
||||||
/// Takes a callback as the only argument. The callback will be updated on every render to make
|
/// Takes a callback as the only argument. The callback will be updated on every render to make
|
||||||
/// sure captured values (if any) are up to date.
|
/// sure captured values (if any) are up to date.
|
||||||
|
#[hook]
|
||||||
pub fn use_bridge<T, F>(on_output: F) -> UseBridgeHandle<T>
|
pub fn use_bridge<T, F>(on_output: F) -> UseBridgeHandle<T>
|
||||||
where
|
where
|
||||||
T: Bridged,
|
T: Bridged,
|
||||||
|
|||||||
@ -21,7 +21,9 @@ lazy_static = "1"
|
|||||||
proc-macro-error = "1"
|
proc-macro-error = "1"
|
||||||
proc-macro2 = "1"
|
proc-macro2 = "1"
|
||||||
quote = "1"
|
quote = "1"
|
||||||
syn = { version = "1", features = ["full", "extra-traits"] }
|
syn = { version = "1", features = ["full", "extra-traits", "visit-mut"] }
|
||||||
|
once_cell = "1"
|
||||||
|
prettyplease = "0.1.1"
|
||||||
|
|
||||||
# testing
|
# testing
|
||||||
[dev-dependencies]
|
[dev-dependencies]
|
||||||
|
|||||||
@ -3,7 +3,11 @@ use quote::{format_ident, quote, ToTokens};
|
|||||||
use syn::parse::{Parse, ParseStream};
|
use syn::parse::{Parse, ParseStream};
|
||||||
use syn::punctuated::Punctuated;
|
use syn::punctuated::Punctuated;
|
||||||
use syn::token::{Comma, Fn};
|
use syn::token::{Comma, Fn};
|
||||||
use syn::{Attribute, Block, FnArg, Generics, Ident, Item, ItemFn, ReturnType, Type, Visibility};
|
use syn::{
|
||||||
|
visit_mut, Attribute, Block, FnArg, Generics, Ident, Item, ItemFn, ReturnType, Type, Visibility,
|
||||||
|
};
|
||||||
|
|
||||||
|
use crate::hook::BodyRewriter;
|
||||||
|
|
||||||
#[derive(Clone)]
|
#[derive(Clone)]
|
||||||
pub struct FunctionComponent {
|
pub struct FunctionComponent {
|
||||||
@ -169,7 +173,7 @@ fn print_fn(func_comp: FunctionComponent, use_fn_name: bool) -> TokenStream {
|
|||||||
fn_token,
|
fn_token,
|
||||||
name,
|
name,
|
||||||
attrs,
|
attrs,
|
||||||
block,
|
mut block,
|
||||||
return_type,
|
return_type,
|
||||||
generics,
|
generics,
|
||||||
arg,
|
arg,
|
||||||
@ -184,9 +188,14 @@ fn print_fn(func_comp: FunctionComponent, use_fn_name: bool) -> TokenStream {
|
|||||||
Ident::new("inner", Span::mixed_site())
|
Ident::new("inner", Span::mixed_site())
|
||||||
};
|
};
|
||||||
|
|
||||||
|
let ctx_ident = Ident::new("ctx", Span::mixed_site());
|
||||||
|
|
||||||
|
let mut body_rewriter = BodyRewriter::default();
|
||||||
|
visit_mut::visit_block_mut(&mut body_rewriter, &mut *block);
|
||||||
|
|
||||||
quote! {
|
quote! {
|
||||||
#(#attrs)*
|
#(#attrs)*
|
||||||
#fn_token #name #ty_generics (#arg) -> #return_type
|
#fn_token #name #ty_generics (#ctx_ident: &mut ::yew::functional::HookContext, #arg) -> #return_type
|
||||||
#where_clause
|
#where_clause
|
||||||
{
|
{
|
||||||
#block
|
#block
|
||||||
@ -241,6 +250,8 @@ pub fn function_component_impl(
|
|||||||
Ident::new("inner", Span::mixed_site())
|
Ident::new("inner", Span::mixed_site())
|
||||||
};
|
};
|
||||||
|
|
||||||
|
let ctx_ident = Ident::new("ctx", Span::mixed_site());
|
||||||
|
|
||||||
let quoted = quote! {
|
let quoted = quote! {
|
||||||
#[doc(hidden)]
|
#[doc(hidden)]
|
||||||
#[allow(non_camel_case_types)]
|
#[allow(non_camel_case_types)]
|
||||||
@ -253,10 +264,10 @@ pub fn function_component_impl(
|
|||||||
impl #impl_generics ::yew::functional::FunctionProvider for #provider_name #ty_generics #where_clause {
|
impl #impl_generics ::yew::functional::FunctionProvider for #provider_name #ty_generics #where_clause {
|
||||||
type TProps = #props_type;
|
type TProps = #props_type;
|
||||||
|
|
||||||
fn run(#provider_props: &Self::TProps) -> ::yew::html::HtmlResult {
|
fn run(#ctx_ident: &mut ::yew::functional::HookContext, #provider_props: &Self::TProps) -> ::yew::html::HtmlResult {
|
||||||
#func
|
#func
|
||||||
|
|
||||||
::yew::html::IntoHtmlResult::into_html_result(#fn_name #fn_generics (#provider_props))
|
::yew::html::IntoHtmlResult::into_html_result(#fn_name #fn_generics (#ctx_ident, #provider_props))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
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 classes;
|
||||||
mod derive_props;
|
mod derive_props;
|
||||||
mod function_component;
|
mod function_component;
|
||||||
|
mod hook;
|
||||||
mod html_tree;
|
mod html_tree;
|
||||||
mod props;
|
mod props;
|
||||||
mod stringify;
|
mod stringify;
|
||||||
|
|
||||||
use derive_props::DerivePropsInput;
|
use derive_props::DerivePropsInput;
|
||||||
use function_component::{function_component_impl, FunctionComponent, FunctionComponentName};
|
use function_component::{function_component_impl, FunctionComponent, FunctionComponentName};
|
||||||
|
use hook::{hook_impl, HookFn};
|
||||||
use html_tree::{HtmlRoot, HtmlRootVNode};
|
use html_tree::{HtmlRoot, HtmlRootVNode};
|
||||||
use proc_macro::TokenStream;
|
use proc_macro::TokenStream;
|
||||||
use quote::ToTokens;
|
use quote::ToTokens;
|
||||||
@ -122,11 +124,9 @@ pub fn classes(input: TokenStream) -> TokenStream {
|
|||||||
TokenStream::from(classes.into_token_stream())
|
TokenStream::from(classes.into_token_stream())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[proc_macro_error::proc_macro_error]
|
||||||
#[proc_macro_attribute]
|
#[proc_macro_attribute]
|
||||||
pub fn function_component(
|
pub fn function_component(attr: TokenStream, item: TokenStream) -> proc_macro::TokenStream {
|
||||||
attr: proc_macro::TokenStream,
|
|
||||||
item: proc_macro::TokenStream,
|
|
||||||
) -> proc_macro::TokenStream {
|
|
||||||
let item = parse_macro_input!(item as FunctionComponent);
|
let item = parse_macro_input!(item as FunctionComponent);
|
||||||
let attr = parse_macro_input!(attr as FunctionComponentName);
|
let attr = parse_macro_input!(attr as FunctionComponentName);
|
||||||
|
|
||||||
@ -134,3 +134,19 @@ pub fn function_component(
|
|||||||
.unwrap_or_else(|err| err.to_compile_error())
|
.unwrap_or_else(|err| err.to_compile_error())
|
||||||
.into()
|
.into()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[proc_macro_error::proc_macro_error]
|
||||||
|
#[proc_macro_attribute]
|
||||||
|
pub fn hook(attr: TokenStream, item: TokenStream) -> proc_macro::TokenStream {
|
||||||
|
let item = parse_macro_input!(item as HookFn);
|
||||||
|
|
||||||
|
if let Some(m) = proc_macro2::TokenStream::from(attr).into_iter().next() {
|
||||||
|
return syn::Error::new_spanned(m, "hook attribute does not accept any arguments")
|
||||||
|
.into_compile_error()
|
||||||
|
.into();
|
||||||
|
}
|
||||||
|
|
||||||
|
hook_impl(item)
|
||||||
|
.unwrap_or_else(|err| err.to_compile_error())
|
||||||
|
.into()
|
||||||
|
}
|
||||||
|
|||||||
@ -29,36 +29,36 @@ error[E0277]: the trait bound `FunctionComponent<CompFunctionProvider<MissingTyp
|
|||||||
<FunctionComponent<T> as BaseComponent>
|
<FunctionComponent<T> as BaseComponent>
|
||||||
|
|
||||||
error[E0599]: the function or associated item `new` exists for struct `VChild<FunctionComponent<CompFunctionProvider<MissingTypeBounds>>>`, but its trait bounds were not satisfied
|
error[E0599]: the function or associated item `new` exists for struct `VChild<FunctionComponent<CompFunctionProvider<MissingTypeBounds>>>`, but its trait bounds were not satisfied
|
||||||
--> tests/function_component_attr/generic-props-fail.rs:27:14
|
--> tests/function_component_attr/generic-props-fail.rs:27:14
|
||||||
|
|
|
|
||||||
27 | html! { <Comp<MissingTypeBounds> /> };
|
27 | html! { <Comp<MissingTypeBounds> /> };
|
||||||
| ^^^^ function or associated item cannot be called on `VChild<FunctionComponent<CompFunctionProvider<MissingTypeBounds>>>` due to unsatisfied trait bounds
|
| ^^^^ function or associated item cannot be called on `VChild<FunctionComponent<CompFunctionProvider<MissingTypeBounds>>>` due to unsatisfied trait bounds
|
||||||
|
|
|
|
||||||
::: $WORKSPACE/packages/yew/src/functional/mod.rs
|
::: $WORKSPACE/packages/yew/src/functional/mod.rs
|
||||||
|
|
|
|
||||||
| pub struct FunctionComponent<T: FunctionProvider + 'static> {
|
| pub struct FunctionComponent<T: FunctionProvider + 'static> {
|
||||||
| ----------------------------------------------------------- doesn't satisfy `_: BaseComponent`
|
| ----------------------------------------------------------- doesn't satisfy `_: BaseComponent`
|
||||||
|
|
|
|
||||||
= note: the following trait bounds were not satisfied:
|
= note: the following trait bounds were not satisfied:
|
||||||
`FunctionComponent<CompFunctionProvider<MissingTypeBounds>>: BaseComponent`
|
`FunctionComponent<CompFunctionProvider<MissingTypeBounds>>: BaseComponent`
|
||||||
|
|
||||||
error[E0277]: the trait bound `MissingTypeBounds: yew::Properties` is not satisfied
|
error[E0277]: the trait bound `MissingTypeBounds: yew::Properties` is not satisfied
|
||||||
--> tests/function_component_attr/generic-props-fail.rs:27:14
|
--> tests/function_component_attr/generic-props-fail.rs:27:14
|
||||||
|
|
|
|
||||||
27 | html! { <Comp<MissingTypeBounds> /> };
|
27 | html! { <Comp<MissingTypeBounds> /> };
|
||||||
| ^^^^ the trait `yew::Properties` is not implemented for `MissingTypeBounds`
|
| ^^^^ the trait `yew::Properties` is not implemented for `MissingTypeBounds`
|
||||||
|
|
|
|
||||||
note: required because of the requirements on the impl of `FunctionProvider` for `CompFunctionProvider<MissingTypeBounds>`
|
note: required because of the requirements on the impl of `FunctionProvider` for `CompFunctionProvider<MissingTypeBounds>`
|
||||||
--> tests/function_component_attr/generic-props-fail.rs:8:1
|
--> tests/function_component_attr/generic-props-fail.rs:8:1
|
||||||
|
|
|
|
||||||
8 | #[function_component(Comp)]
|
8 | #[function_component(Comp)]
|
||||||
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^
|
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^
|
||||||
note: required by a bound in `FunctionComponent`
|
note: required by a bound in `FunctionComponent`
|
||||||
--> $WORKSPACE/packages/yew/src/functional/mod.rs
|
--> $WORKSPACE/packages/yew/src/functional/mod.rs
|
||||||
|
|
|
|
||||||
| pub struct FunctionComponent<T: FunctionProvider + 'static> {
|
| pub struct FunctionComponent<T: FunctionProvider + 'static> {
|
||||||
| ^^^^^^^^^^^^^^^^ required by this bound in `FunctionComponent`
|
| ^^^^^^^^^^^^^^^^ required by this bound in `FunctionComponent`
|
||||||
= note: this error originates in the attribute macro `function_component` (in Nightly builds, run with -Z macro-backtrace for more info)
|
= note: this error originates in the attribute macro `function_component` (in Nightly builds, run with -Z macro-backtrace for more info)
|
||||||
|
|
||||||
error[E0107]: missing generics for type alias `Comp`
|
error[E0107]: missing generics for type alias `Comp`
|
||||||
--> tests/function_component_attr/generic-props-fail.rs:30:14
|
--> tests/function_component_attr/generic-props-fail.rs:30:14
|
||||||
|
|||||||
@ -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::*;
|
use yew::prelude::*;
|
||||||
|
|
||||||
/// A hook to access the [`Navigator`].
|
/// A hook to access the [`Navigator`].
|
||||||
|
#[hook]
|
||||||
pub fn use_navigator() -> Option<Navigator> {
|
pub fn use_navigator() -> Option<Navigator> {
|
||||||
use_context::<NavigatorContext>().map(|m| m.navigator())
|
use_context::<NavigatorContext>().map(|m| m.navigator())
|
||||||
}
|
}
|
||||||
|
|
||||||
/// A hook to access the current [`Location`].
|
/// A hook to access the current [`Location`].
|
||||||
|
#[hook]
|
||||||
pub fn use_location() -> Option<Location> {
|
pub fn use_location() -> Option<Location> {
|
||||||
Some(use_context::<LocationContext>()?.location())
|
Some(use_context::<LocationContext>()?.location())
|
||||||
}
|
}
|
||||||
@ -25,6 +27,7 @@ pub fn use_location() -> Option<Location> {
|
|||||||
///
|
///
|
||||||
/// If your `Routable` has a `#[not_found]` route, you can use `.unwrap_or_default()` instead of
|
/// If your `Routable` has a `#[not_found]` route, you can use `.unwrap_or_default()` instead of
|
||||||
/// `.unwrap()` to unwrap.
|
/// `.unwrap()` to unwrap.
|
||||||
|
#[hook]
|
||||||
pub fn use_route<R>() -> Option<R>
|
pub fn use_route<R>() -> Option<R>
|
||||||
where
|
where
|
||||||
R: Routable + 'static,
|
R: Routable + 'static,
|
||||||
|
|||||||
@ -27,8 +27,6 @@ wasm-bindgen = "0.2"
|
|||||||
yew-macro = { version = "^0.19.0", path = "../yew-macro" }
|
yew-macro = { version = "^0.19.0", path = "../yew-macro" }
|
||||||
thiserror = "1.0"
|
thiserror = "1.0"
|
||||||
|
|
||||||
scoped-tls-hkt = "0.1"
|
|
||||||
|
|
||||||
futures = { version = "0.3", optional = true }
|
futures = { version = "0.3", optional = true }
|
||||||
html-escape = { version = "0.2.9", optional = true }
|
html-escape = { version = "0.2.9", optional = true }
|
||||||
|
|
||||||
|
|||||||
@ -1,75 +1,64 @@
|
|||||||
mod use_context;
|
mod use_context;
|
||||||
mod use_effect;
|
mod use_effect;
|
||||||
|
mod use_memo;
|
||||||
mod use_reducer;
|
mod use_reducer;
|
||||||
mod use_ref;
|
mod use_ref;
|
||||||
mod use_state;
|
mod use_state;
|
||||||
|
|
||||||
pub use use_context::*;
|
pub use use_context::*;
|
||||||
pub use use_effect::*;
|
pub use use_effect::*;
|
||||||
|
pub use use_memo::*;
|
||||||
pub use use_reducer::*;
|
pub use use_reducer::*;
|
||||||
pub use use_ref::*;
|
pub use use_ref::*;
|
||||||
pub use use_state::*;
|
pub use use_state::*;
|
||||||
|
|
||||||
use crate::functional::{HookUpdater, CURRENT_HOOK};
|
use crate::functional::{AnyScope, HookContext};
|
||||||
use std::cell::RefCell;
|
|
||||||
use std::ops::DerefMut;
|
|
||||||
use std::rc::Rc;
|
|
||||||
|
|
||||||
/// Low level building block of creating hooks.
|
/// A trait that is implemented on hooks.
|
||||||
///
|
///
|
||||||
/// It is used to created the pre-defined primitive hooks.
|
/// Hooks are defined via the [`#[hook]`](crate::functional::hook) macro. It provides rewrites to hook invocations
|
||||||
/// Generally, it isn't needed to create hooks and should be avoided as most custom hooks can be
|
/// and ensures that hooks can only be called at the top-level of a function component or a hook.
|
||||||
/// created by combining other hooks as described in [Yew Docs].
|
/// Please refer to its documentation on how to implement hooks.
|
||||||
///
|
pub trait Hook {
|
||||||
/// The `initializer` callback is called once to create the initial state of the hook.
|
/// The return type when a hook is run.
|
||||||
/// `runner` callback handles the logic of the hook. It is called when the hook function is called.
|
type Output;
|
||||||
/// `destructor`, as the name implies, is called to cleanup the leftovers of the hook.
|
|
||||||
///
|
/// Runs the hook inside current state, returns output upon completion.
|
||||||
/// See the pre-defined hooks for examples of how to use this function.
|
fn run(self, ctx: &mut HookContext) -> Self::Output;
|
||||||
///
|
}
|
||||||
/// [Yew Docs]: https://yew.rs/next/concepts/function-components/custom-hooks
|
|
||||||
pub fn use_hook<InternalHook: 'static, Output, Tear: FnOnce(&mut InternalHook) + 'static>(
|
/// The blanket implementation of boxed hooks.
|
||||||
initializer: impl FnOnce() -> InternalHook,
|
#[doc(hidden)]
|
||||||
runner: impl FnOnce(&mut InternalHook, HookUpdater) -> Output,
|
#[allow(missing_debug_implementations, missing_docs)]
|
||||||
destructor: Tear,
|
pub struct BoxedHook<'hook, T> {
|
||||||
) -> Output {
|
inner: Box<dyn 'hook + FnOnce(&mut HookContext) -> T>,
|
||||||
if !CURRENT_HOOK.is_set() {
|
}
|
||||||
panic!("Hooks can only be used in the scope of a function component");
|
|
||||||
|
impl<'hook, T> BoxedHook<'hook, T> {
|
||||||
|
#[allow(missing_docs)]
|
||||||
|
pub fn new(inner: Box<dyn 'hook + FnOnce(&mut HookContext) -> T>) -> Self {
|
||||||
|
Self { inner }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<T> Hook for BoxedHook<'_, T> {
|
||||||
|
type Output = T;
|
||||||
|
|
||||||
|
fn run(self, ctx: &mut HookContext) -> Self::Output {
|
||||||
|
(self.inner)(ctx)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(crate) fn use_component_scope() -> impl Hook<Output = AnyScope> {
|
||||||
|
struct HookProvider {}
|
||||||
|
|
||||||
|
impl Hook for HookProvider {
|
||||||
|
type Output = AnyScope;
|
||||||
|
|
||||||
|
fn run(self, ctx: &mut HookContext) -> Self::Output {
|
||||||
|
ctx.scope.clone()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Extract current hook
|
HookProvider {}
|
||||||
let updater = CURRENT_HOOK.with(|hook_state| {
|
|
||||||
// Determine which hook position we're at and increment for the next hook
|
|
||||||
let hook_pos = hook_state.counter;
|
|
||||||
hook_state.counter += 1;
|
|
||||||
|
|
||||||
// Initialize hook if this is the first call
|
|
||||||
if hook_pos >= hook_state.hooks.len() {
|
|
||||||
let initial_state = Rc::new(RefCell::new(initializer()));
|
|
||||||
hook_state.hooks.push(initial_state.clone());
|
|
||||||
hook_state.destroy_listeners.push(Box::new(move || {
|
|
||||||
destructor(initial_state.borrow_mut().deref_mut());
|
|
||||||
}));
|
|
||||||
}
|
|
||||||
|
|
||||||
let hook = hook_state
|
|
||||||
.hooks
|
|
||||||
.get(hook_pos)
|
|
||||||
.expect("Not the same number of hooks. Hooks must not be called conditionally")
|
|
||||||
.clone();
|
|
||||||
|
|
||||||
HookUpdater {
|
|
||||||
hook,
|
|
||||||
process_message: hook_state.process_message.clone(),
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
// Execute the actual hook closure we were given. Let it mutate the hook state and let
|
|
||||||
// it create a callback that takes the mutable hook state.
|
|
||||||
let mut hook = updater.hook.borrow_mut();
|
|
||||||
let hook: &mut InternalHook = hook
|
|
||||||
.downcast_mut()
|
|
||||||
.expect("Incompatible hook type. Hooks must always be called in the same order");
|
|
||||||
|
|
||||||
runner(hook, updater.clone())
|
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,5 +1,6 @@
|
|||||||
|
use crate::callback::Callback;
|
||||||
use crate::context::ContextHandle;
|
use crate::context::ContextHandle;
|
||||||
use crate::functional::{get_current_scope, use_hook};
|
use crate::functional::{hook, use_component_scope, use_memo, use_state};
|
||||||
|
|
||||||
/// Hook for consuming context values in function components.
|
/// Hook for consuming context values in function components.
|
||||||
/// The context of the type passed as `T` is returned. If there is no such context in scope, `None` is returned.
|
/// The context of the type passed as `T` is returned. If there is no such context in scope, `None` is returned.
|
||||||
@ -28,38 +29,29 @@ use crate::functional::{get_current_scope, use_hook};
|
|||||||
/// }
|
/// }
|
||||||
/// }
|
/// }
|
||||||
/// ```
|
/// ```
|
||||||
|
#[hook]
|
||||||
pub fn use_context<T: Clone + PartialEq + 'static>() -> Option<T> {
|
pub fn use_context<T: Clone + PartialEq + 'static>() -> Option<T> {
|
||||||
struct UseContextState<T2: Clone + PartialEq + 'static> {
|
struct UseContext<T: Clone + PartialEq + 'static> {
|
||||||
initialized: bool,
|
context: Option<(T, ContextHandle<T>)>,
|
||||||
context: Option<(T2, ContextHandle<T2>)>,
|
|
||||||
}
|
}
|
||||||
|
|
||||||
let scope = get_current_scope()
|
let scope = use_component_scope();
|
||||||
.expect("No current Scope. `use_context` can only be called inside function components");
|
|
||||||
|
|
||||||
use_hook(
|
let val = use_state(|| -> Option<T> { None });
|
||||||
move || UseContextState {
|
let state = {
|
||||||
initialized: false,
|
let val_dispatcher = val.setter();
|
||||||
context: None,
|
use_memo(
|
||||||
},
|
move |_| UseContext {
|
||||||
|state: &mut UseContextState<T>, updater| {
|
context: scope.context::<T>(Callback::from(move |m| {
|
||||||
if !state.initialized {
|
val_dispatcher.clone().set(Some(m));
|
||||||
state.initialized = true;
|
})),
|
||||||
let callback = move |ctx: T| {
|
},
|
||||||
updater.callback(|state: &mut UseContextState<T>| {
|
(),
|
||||||
if let Some(context) = &mut state.context {
|
)
|
||||||
context.0 = ctx;
|
};
|
||||||
}
|
|
||||||
true
|
|
||||||
});
|
|
||||||
};
|
|
||||||
state.context = scope.context::<T>(callback.into());
|
|
||||||
}
|
|
||||||
|
|
||||||
Some(state.context.as_ref()?.0.clone())
|
// we fallback to initial value if it was not updated.
|
||||||
},
|
(*val)
|
||||||
|state| {
|
.clone()
|
||||||
state.context = None;
|
.or_else(move || state.context.as_ref().map(|m| m.0.clone()))
|
||||||
},
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,9 +1,111 @@
|
|||||||
use crate::functional::use_hook;
|
use std::cell::RefCell;
|
||||||
use std::rc::Rc;
|
|
||||||
|
|
||||||
struct UseEffect<Destructor> {
|
use crate::functional::{hook, Effect, Hook, HookContext};
|
||||||
runner: Option<Box<dyn FnOnce() -> Destructor>>,
|
|
||||||
destructor: Option<Box<Destructor>>,
|
struct UseEffectBase<T, F, D>
|
||||||
|
where
|
||||||
|
F: FnOnce(&T) -> D + 'static,
|
||||||
|
T: 'static,
|
||||||
|
D: FnOnce() + 'static,
|
||||||
|
{
|
||||||
|
runner_with_deps: Option<(T, F)>,
|
||||||
|
destructor: Option<D>,
|
||||||
|
deps: Option<T>,
|
||||||
|
effect_changed_fn: fn(Option<&T>, Option<&T>) -> bool,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<T, F, D> Effect for RefCell<UseEffectBase<T, F, D>>
|
||||||
|
where
|
||||||
|
F: FnOnce(&T) -> D + 'static,
|
||||||
|
T: 'static,
|
||||||
|
D: FnOnce() + 'static,
|
||||||
|
{
|
||||||
|
fn rendered(&self) {
|
||||||
|
let mut this = self.borrow_mut();
|
||||||
|
|
||||||
|
if let Some((deps, runner)) = this.runner_with_deps.take() {
|
||||||
|
if !(this.effect_changed_fn)(Some(&deps), this.deps.as_ref()) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if let Some(de) = this.destructor.take() {
|
||||||
|
de();
|
||||||
|
}
|
||||||
|
|
||||||
|
let new_destructor = runner(&deps);
|
||||||
|
|
||||||
|
this.deps = Some(deps);
|
||||||
|
this.destructor = Some(new_destructor);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<T, F, D> Drop for UseEffectBase<T, F, D>
|
||||||
|
where
|
||||||
|
F: FnOnce(&T) -> D + 'static,
|
||||||
|
T: 'static,
|
||||||
|
D: FnOnce() + 'static,
|
||||||
|
{
|
||||||
|
fn drop(&mut self) {
|
||||||
|
if let Some(destructor) = self.destructor.take() {
|
||||||
|
destructor()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn use_effect_base<T, D>(
|
||||||
|
runner: impl FnOnce(&T) -> D + 'static,
|
||||||
|
deps: T,
|
||||||
|
effect_changed_fn: fn(Option<&T>, Option<&T>) -> bool,
|
||||||
|
) -> impl Hook<Output = ()>
|
||||||
|
where
|
||||||
|
T: 'static,
|
||||||
|
D: FnOnce() + 'static,
|
||||||
|
{
|
||||||
|
struct HookProvider<T, F, D>
|
||||||
|
where
|
||||||
|
F: FnOnce(&T) -> D + 'static,
|
||||||
|
T: 'static,
|
||||||
|
D: FnOnce() + 'static,
|
||||||
|
{
|
||||||
|
runner: F,
|
||||||
|
deps: T,
|
||||||
|
effect_changed_fn: fn(Option<&T>, Option<&T>) -> bool,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<T, F, D> Hook for HookProvider<T, F, D>
|
||||||
|
where
|
||||||
|
F: FnOnce(&T) -> D + 'static,
|
||||||
|
T: 'static,
|
||||||
|
D: FnOnce() + 'static,
|
||||||
|
{
|
||||||
|
type Output = ();
|
||||||
|
|
||||||
|
fn run(self, ctx: &mut HookContext) -> Self::Output {
|
||||||
|
let Self {
|
||||||
|
runner,
|
||||||
|
deps,
|
||||||
|
effect_changed_fn,
|
||||||
|
} = self;
|
||||||
|
|
||||||
|
let state = ctx.next_effect(|_| -> RefCell<UseEffectBase<T, F, D>> {
|
||||||
|
RefCell::new(UseEffectBase {
|
||||||
|
runner_with_deps: None,
|
||||||
|
destructor: None,
|
||||||
|
deps: None,
|
||||||
|
effect_changed_fn,
|
||||||
|
})
|
||||||
|
});
|
||||||
|
|
||||||
|
state.borrow_mut().runner_with_deps = Some((deps, runner));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
HookProvider {
|
||||||
|
runner,
|
||||||
|
deps,
|
||||||
|
effect_changed_fn,
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// This hook is used for hooking into the component's lifecycle.
|
/// This hook is used for hooking into the component's lifecycle.
|
||||||
@ -36,51 +138,13 @@ struct UseEffect<Destructor> {
|
|||||||
/// }
|
/// }
|
||||||
/// }
|
/// }
|
||||||
/// ```
|
/// ```
|
||||||
pub fn use_effect<Destructor>(callback: impl FnOnce() -> Destructor + 'static)
|
#[hook]
|
||||||
|
pub fn use_effect<F, D>(f: F)
|
||||||
where
|
where
|
||||||
Destructor: FnOnce() + 'static,
|
F: FnOnce() -> D + 'static,
|
||||||
|
D: FnOnce() + 'static,
|
||||||
{
|
{
|
||||||
use_hook(
|
use_effect_base(|_| f(), (), |_, _| true);
|
||||||
move || {
|
|
||||||
let effect: UseEffect<Destructor> = UseEffect {
|
|
||||||
runner: None,
|
|
||||||
destructor: None,
|
|
||||||
};
|
|
||||||
effect
|
|
||||||
},
|
|
||||||
|state, updater| {
|
|
||||||
state.runner = Some(Box::new(callback) as Box<dyn FnOnce() -> Destructor>);
|
|
||||||
|
|
||||||
// Run on every render
|
|
||||||
updater.post_render(move |state: &mut UseEffect<Destructor>| {
|
|
||||||
if let Some(callback) = state.runner.take() {
|
|
||||||
if let Some(de) = state.destructor.take() {
|
|
||||||
de();
|
|
||||||
}
|
|
||||||
|
|
||||||
let new_destructor = callback();
|
|
||||||
state.destructor.replace(Box::new(new_destructor));
|
|
||||||
}
|
|
||||||
false
|
|
||||||
});
|
|
||||||
},
|
|
||||||
|hook| {
|
|
||||||
if let Some(destructor) = hook.destructor.take() {
|
|
||||||
destructor()
|
|
||||||
}
|
|
||||||
},
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
type UseEffectDepsRunnerFn<Dependents, Destructor> = Box<dyn FnOnce(&Dependents) -> Destructor>;
|
|
||||||
|
|
||||||
struct UseEffectDeps<Destructor, Dependents> {
|
|
||||||
runner_with_deps: Option<(
|
|
||||||
Rc<Dependents>,
|
|
||||||
UseEffectDepsRunnerFn<Dependents, Destructor>,
|
|
||||||
)>,
|
|
||||||
destructor: Option<Box<Destructor>>,
|
|
||||||
deps: Option<Rc<Dependents>>,
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// This hook is similar to [`use_effect`] but it accepts dependencies.
|
/// This hook is similar to [`use_effect`] but it accepts dependencies.
|
||||||
@ -88,48 +152,12 @@ struct UseEffectDeps<Destructor, Dependents> {
|
|||||||
/// Whenever the dependencies are changed, the effect callback is called again.
|
/// Whenever the dependencies are changed, the effect callback is called again.
|
||||||
/// To detect changes, dependencies must implement `PartialEq`.
|
/// To detect changes, dependencies must implement `PartialEq`.
|
||||||
/// Note that the destructor also runs when dependencies change.
|
/// Note that the destructor also runs when dependencies change.
|
||||||
pub fn use_effect_with_deps<Callback, Destructor, Dependents>(callback: Callback, deps: Dependents)
|
#[hook]
|
||||||
|
pub fn use_effect_with_deps<T, F, D>(f: F, deps: T)
|
||||||
where
|
where
|
||||||
Callback: FnOnce(&Dependents) -> Destructor + 'static,
|
T: PartialEq + 'static,
|
||||||
Destructor: FnOnce() + 'static,
|
F: FnOnce(&T) -> D + 'static,
|
||||||
Dependents: PartialEq + 'static,
|
D: FnOnce() + 'static,
|
||||||
{
|
{
|
||||||
let deps = Rc::new(deps);
|
use_effect_base(f, deps, |lhs, rhs| lhs != rhs)
|
||||||
|
|
||||||
use_hook(
|
|
||||||
move || {
|
|
||||||
let destructor: Option<Box<Destructor>> = None;
|
|
||||||
UseEffectDeps {
|
|
||||||
runner_with_deps: None,
|
|
||||||
destructor,
|
|
||||||
deps: None,
|
|
||||||
}
|
|
||||||
},
|
|
||||||
move |state, updater| {
|
|
||||||
state.runner_with_deps = Some((deps, Box::new(callback)));
|
|
||||||
|
|
||||||
updater.post_render(move |state: &mut UseEffectDeps<Destructor, Dependents>| {
|
|
||||||
if let Some((deps, callback)) = state.runner_with_deps.take() {
|
|
||||||
if Some(&deps) == state.deps.as_ref() {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
if let Some(de) = state.destructor.take() {
|
|
||||||
de();
|
|
||||||
}
|
|
||||||
|
|
||||||
let new_destructor = callback(&deps);
|
|
||||||
|
|
||||||
state.deps = Some(deps);
|
|
||||||
state.destructor = Some(Box::new(new_destructor));
|
|
||||||
}
|
|
||||||
false
|
|
||||||
});
|
|
||||||
},
|
|
||||||
|hook| {
|
|
||||||
if let Some(destructor) = hook.destructor.take() {
|
|
||||||
destructor()
|
|
||||||
}
|
|
||||||
},
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|||||||
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::cell::RefCell;
|
||||||
use std::fmt;
|
use std::fmt;
|
||||||
|
use std::marker::PhantomData;
|
||||||
use std::ops::Deref;
|
use std::ops::Deref;
|
||||||
use std::rc::Rc;
|
use std::rc::Rc;
|
||||||
|
|
||||||
use crate::functional::use_hook;
|
use crate::functional::{hook, Hook, HookContext};
|
||||||
|
|
||||||
type DispatchFn<T> = Rc<dyn Fn(<T as Reducible>::Action)>;
|
type DispatchFn<T> = Rc<dyn Fn(<T as Reducible>::Action)>;
|
||||||
|
|
||||||
@ -20,10 +21,9 @@ struct UseReducer<T>
|
|||||||
where
|
where
|
||||||
T: Reducible,
|
T: Reducible,
|
||||||
{
|
{
|
||||||
current_state: Rc<T>,
|
current_state: Rc<RefCell<Rc<T>>>,
|
||||||
|
|
||||||
// To be replaced with OnceCell once it becomes available in std.
|
dispatch: DispatchFn<T>,
|
||||||
dispatch: RefCell<Option<DispatchFn<T>>>,
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// State handle for [`use_reducer`] and [`use_reducer_eq`] hook
|
/// State handle for [`use_reducer`] and [`use_reducer_eq`] hook
|
||||||
@ -144,51 +144,76 @@ where
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// The base function of [`use_reducer`] and [`use_reducer_eq`]
|
/// The base function of [`use_reducer`] and [`use_reducer_eq`]
|
||||||
fn use_reducer_base<T, F, R>(initial_fn: F, should_render_fn: R) -> UseReducerHandle<T>
|
fn use_reducer_base<'hook, T>(
|
||||||
|
init_fn: impl 'hook + FnOnce() -> T,
|
||||||
|
should_render_fn: fn(&T, &T) -> bool,
|
||||||
|
) -> impl 'hook + Hook<Output = UseReducerHandle<T>>
|
||||||
where
|
where
|
||||||
T: Reducible + 'static,
|
T: Reducible + 'static,
|
||||||
F: FnOnce() -> T,
|
|
||||||
R: (Fn(&T, &T) -> bool) + 'static,
|
|
||||||
{
|
{
|
||||||
use_hook(
|
struct HookProvider<'hook, T, F>
|
||||||
move || UseReducer {
|
where
|
||||||
current_state: Rc::new(initial_fn()),
|
T: Reducible + 'static,
|
||||||
dispatch: RefCell::default(),
|
F: 'hook + FnOnce() -> T,
|
||||||
},
|
{
|
||||||
|s, updater| {
|
_marker: PhantomData<&'hook ()>,
|
||||||
let mut dispatch_ref = s.dispatch.borrow_mut();
|
|
||||||
|
|
||||||
// Create dispatch once.
|
init_fn: F,
|
||||||
let dispatch = match *dispatch_ref {
|
should_render_fn: fn(&T, &T) -> bool,
|
||||||
Some(ref m) => (*m).to_owned(),
|
}
|
||||||
None => {
|
|
||||||
let should_render_fn = Rc::new(should_render_fn);
|
|
||||||
|
|
||||||
let dispatch: Rc<dyn Fn(T::Action)> = Rc::new(move |action: T::Action| {
|
impl<'hook, T, F> Hook for HookProvider<'hook, T, F>
|
||||||
let should_render_fn = should_render_fn.clone();
|
where
|
||||||
|
T: Reducible + 'static,
|
||||||
|
F: 'hook + FnOnce() -> T,
|
||||||
|
{
|
||||||
|
type Output = UseReducerHandle<T>;
|
||||||
|
|
||||||
updater.callback(move |state: &mut UseReducer<T>| {
|
fn run(self, ctx: &mut HookContext) -> Self::Output {
|
||||||
let next_state = state.current_state.clone().reduce(action);
|
let Self {
|
||||||
let should_render = should_render_fn(&next_state, &state.current_state);
|
init_fn,
|
||||||
state.current_state = next_state;
|
should_render_fn,
|
||||||
|
..
|
||||||
|
} = self;
|
||||||
|
|
||||||
|
let state = ctx.next_state(move |re_render| {
|
||||||
|
let val = Rc::new(RefCell::new(Rc::new(init_fn())));
|
||||||
|
let should_render_fn = Rc::new(should_render_fn);
|
||||||
|
|
||||||
|
UseReducer {
|
||||||
|
current_state: val.clone(),
|
||||||
|
dispatch: Rc::new(move |action: T::Action| {
|
||||||
|
let should_render = {
|
||||||
|
let should_render_fn = should_render_fn.clone();
|
||||||
|
let mut val = val.borrow_mut();
|
||||||
|
let next_val = (*val).clone().reduce(action);
|
||||||
|
let should_render = should_render_fn(&next_val, &val);
|
||||||
|
*val = next_val;
|
||||||
|
|
||||||
should_render
|
should_render
|
||||||
});
|
};
|
||||||
});
|
|
||||||
|
|
||||||
*dispatch_ref = Some(dispatch.clone());
|
// Currently, this triggers a render immediately, so we need to release the
|
||||||
|
// borrowed reference first.
|
||||||
dispatch
|
if should_render {
|
||||||
|
re_render()
|
||||||
|
}
|
||||||
|
}),
|
||||||
}
|
}
|
||||||
};
|
});
|
||||||
|
|
||||||
UseReducerHandle {
|
let value = state.current_state.borrow().clone();
|
||||||
value: Rc::clone(&s.current_state),
|
let dispatch = state.dispatch.clone();
|
||||||
dispatch,
|
|
||||||
}
|
UseReducerHandle { value, dispatch }
|
||||||
},
|
}
|
||||||
|_| {},
|
}
|
||||||
)
|
|
||||||
|
HookProvider {
|
||||||
|
_marker: PhantomData,
|
||||||
|
init_fn,
|
||||||
|
should_render_fn,
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// This hook is an alternative to [`use_state`](super::use_state()).
|
/// This hook is an alternative to [`use_state`](super::use_state()).
|
||||||
@ -259,22 +284,24 @@ where
|
|||||||
/// }
|
/// }
|
||||||
/// }
|
/// }
|
||||||
/// ```
|
/// ```
|
||||||
pub fn use_reducer<T, F>(initial_fn: F) -> UseReducerHandle<T>
|
#[hook]
|
||||||
|
pub fn use_reducer<T, F>(init_fn: F) -> UseReducerHandle<T>
|
||||||
where
|
where
|
||||||
T: Reducible + 'static,
|
T: Reducible + 'static,
|
||||||
F: FnOnce() -> T,
|
F: FnOnce() -> T,
|
||||||
{
|
{
|
||||||
use_reducer_base(initial_fn, |_, _| true)
|
use_reducer_base(init_fn, |_, _| true)
|
||||||
}
|
}
|
||||||
|
|
||||||
/// [`use_reducer`] but only re-renders when `prev_state != next_state`.
|
/// [`use_reducer`] but only re-renders when `prev_state != next_state`.
|
||||||
///
|
///
|
||||||
/// This requires the state to implement [`PartialEq`] in addition to the [`Reducible`] trait
|
/// This requires the state to implement [`PartialEq`] in addition to the [`Reducible`] trait
|
||||||
/// required by [`use_reducer`].
|
/// required by [`use_reducer`].
|
||||||
pub fn use_reducer_eq<T, F>(initial_fn: F) -> UseReducerHandle<T>
|
#[hook]
|
||||||
|
pub fn use_reducer_eq<T, F>(init_fn: F) -> UseReducerHandle<T>
|
||||||
where
|
where
|
||||||
T: Reducible + PartialEq + 'static,
|
T: Reducible + PartialEq + 'static,
|
||||||
F: FnOnce() -> T,
|
F: FnOnce() -> T,
|
||||||
{
|
{
|
||||||
use_reducer_base(initial_fn, T::ne)
|
use_reducer_base(init_fn, T::ne)
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,5 +1,8 @@
|
|||||||
use crate::{functional::use_hook, NodeRef};
|
use std::cell::RefCell;
|
||||||
use std::{cell::RefCell, rc::Rc};
|
use std::rc::Rc;
|
||||||
|
|
||||||
|
use crate::functional::{hook, use_memo, use_state};
|
||||||
|
use crate::NodeRef;
|
||||||
|
|
||||||
/// This hook is used for obtaining a mutable reference to a stateful value.
|
/// This hook is used for obtaining a mutable reference to a stateful value.
|
||||||
/// Its state persists across renders.
|
/// Its state persists across renders.
|
||||||
@ -47,25 +50,12 @@ use std::{cell::RefCell, rc::Rc};
|
|||||||
/// }
|
/// }
|
||||||
/// }
|
/// }
|
||||||
/// ```
|
/// ```
|
||||||
pub fn use_mut_ref<T: 'static>(initial_value: impl FnOnce() -> T) -> Rc<RefCell<T>> {
|
#[hook]
|
||||||
use_hook(
|
pub fn use_mut_ref<T: 'static, F>(init_fn: F) -> Rc<RefCell<T>>
|
||||||
|| Rc::new(RefCell::new(initial_value())),
|
where
|
||||||
|state, _| state.clone(),
|
F: FnOnce() -> T,
|
||||||
|_| {},
|
{
|
||||||
)
|
use_memo(|_| RefCell::new(init_fn()), ())
|
||||||
}
|
|
||||||
|
|
||||||
/// This hook is used for obtaining a immutable reference to a stateful value.
|
|
||||||
/// Its state persists across renders.
|
|
||||||
///
|
|
||||||
/// If you need a mutable reference, consider using [`use_mut_ref`](super::use_mut_ref).
|
|
||||||
/// If you need the component to be re-rendered on state change, consider using [`use_state`](super::use_state()).
|
|
||||||
pub fn use_ref<T: 'static>(initial_value: impl FnOnce() -> T) -> Rc<T> {
|
|
||||||
use_hook(
|
|
||||||
|| Rc::new(initial_value()),
|
|
||||||
|state, _| Rc::clone(state),
|
|
||||||
|_| {},
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// This hook is used for obtaining a [`NodeRef`].
|
/// This hook is used for obtaining a [`NodeRef`].
|
||||||
@ -125,6 +115,7 @@ pub fn use_ref<T: 'static>(initial_value: impl FnOnce() -> T) -> Rc<T> {
|
|||||||
/// }
|
/// }
|
||||||
///
|
///
|
||||||
/// ```
|
/// ```
|
||||||
|
#[hook]
|
||||||
pub fn use_node_ref() -> NodeRef {
|
pub fn use_node_ref() -> NodeRef {
|
||||||
use_hook(NodeRef::default, |state, _| state.clone(), |_| {})
|
(*use_state(NodeRef::default)).clone()
|
||||||
}
|
}
|
||||||
|
|||||||
@ -3,17 +3,16 @@ use std::ops::Deref;
|
|||||||
use std::rc::Rc;
|
use std::rc::Rc;
|
||||||
|
|
||||||
use super::{use_reducer, use_reducer_eq, Reducible, UseReducerDispatcher, UseReducerHandle};
|
use super::{use_reducer, use_reducer_eq, Reducible, UseReducerDispatcher, UseReducerHandle};
|
||||||
|
use crate::functional::hook;
|
||||||
|
|
||||||
struct UseStateReducer<T> {
|
struct UseStateReducer<T> {
|
||||||
value: Rc<T>,
|
value: T,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl<T> Reducible for UseStateReducer<T> {
|
impl<T> Reducible for UseStateReducer<T> {
|
||||||
type Action = T;
|
type Action = T;
|
||||||
fn reduce(self: Rc<Self>, action: Self::Action) -> Rc<Self> {
|
fn reduce(self: Rc<Self>, action: Self::Action) -> Rc<Self> {
|
||||||
Rc::new(Self {
|
Rc::new(Self { value: action })
|
||||||
value: action.into(),
|
|
||||||
})
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -53,14 +52,13 @@ where
|
|||||||
/// }
|
/// }
|
||||||
/// }
|
/// }
|
||||||
/// ```
|
/// ```
|
||||||
|
#[hook]
|
||||||
pub fn use_state<T, F>(init_fn: F) -> UseStateHandle<T>
|
pub fn use_state<T, F>(init_fn: F) -> UseStateHandle<T>
|
||||||
where
|
where
|
||||||
T: 'static,
|
T: 'static,
|
||||||
F: FnOnce() -> T,
|
F: FnOnce() -> T,
|
||||||
{
|
{
|
||||||
let handle = use_reducer(move || UseStateReducer {
|
let handle = use_reducer(move || UseStateReducer { value: init_fn() });
|
||||||
value: Rc::new(init_fn()),
|
|
||||||
});
|
|
||||||
|
|
||||||
UseStateHandle { inner: handle }
|
UseStateHandle { inner: handle }
|
||||||
}
|
}
|
||||||
@ -68,14 +66,13 @@ where
|
|||||||
/// [`use_state`] but only re-renders when `prev_state != next_state`.
|
/// [`use_state`] but only re-renders when `prev_state != next_state`.
|
||||||
///
|
///
|
||||||
/// This hook requires the state to implement [`PartialEq`].
|
/// This hook requires the state to implement [`PartialEq`].
|
||||||
|
#[hook]
|
||||||
pub fn use_state_eq<T, F>(init_fn: F) -> UseStateHandle<T>
|
pub fn use_state_eq<T, F>(init_fn: F) -> UseStateHandle<T>
|
||||||
where
|
where
|
||||||
T: PartialEq + 'static,
|
T: PartialEq + 'static,
|
||||||
F: FnOnce() -> T,
|
F: FnOnce() -> T,
|
||||||
{
|
{
|
||||||
let handle = use_reducer_eq(move || UseStateReducer {
|
let handle = use_reducer_eq(move || UseStateReducer { value: init_fn() });
|
||||||
value: Rc::new(init_fn()),
|
|
||||||
});
|
|
||||||
|
|
||||||
UseStateHandle { inner: handle }
|
UseStateHandle { inner: handle }
|
||||||
}
|
}
|
||||||
|
|||||||
@ -15,7 +15,7 @@
|
|||||||
|
|
||||||
use crate::html::{AnyScope, BaseComponent, HtmlResult};
|
use crate::html::{AnyScope, BaseComponent, HtmlResult};
|
||||||
use crate::Properties;
|
use crate::Properties;
|
||||||
use scoped_tls_hkt::scoped_thread_local;
|
use std::any::Any;
|
||||||
use std::cell::RefCell;
|
use std::cell::RefCell;
|
||||||
use std::fmt;
|
use std::fmt;
|
||||||
use std::rc::Rc;
|
use std::rc::Rc;
|
||||||
@ -55,17 +55,71 @@ use crate::html::SealedBaseComponent;
|
|||||||
/// ```
|
/// ```
|
||||||
pub use yew_macro::function_component;
|
pub use yew_macro::function_component;
|
||||||
|
|
||||||
scoped_thread_local!(static mut CURRENT_HOOK: HookState);
|
/// This attribute creates a user-defined hook from a normal Rust function.
|
||||||
|
pub use yew_macro::hook;
|
||||||
|
|
||||||
type Msg = Box<dyn FnOnce() -> bool>;
|
type ReRender = Rc<dyn Fn()>;
|
||||||
type ProcessMessage = Rc<dyn Fn(Msg, bool)>;
|
|
||||||
|
/// Primitives of a Hook state.
|
||||||
|
pub(crate) trait Effect {
|
||||||
|
fn rendered(&self) {}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// A hook context to be passed to hooks.
|
||||||
|
pub struct HookContext {
|
||||||
|
pub(crate) scope: AnyScope,
|
||||||
|
re_render: ReRender,
|
||||||
|
|
||||||
|
states: Vec<Rc<dyn Any>>,
|
||||||
|
effects: Vec<Rc<dyn Effect>>,
|
||||||
|
|
||||||
struct HookState {
|
|
||||||
counter: usize,
|
counter: usize,
|
||||||
scope: AnyScope,
|
#[cfg(debug_assertions)]
|
||||||
process_message: ProcessMessage,
|
total_hook_counter: Option<usize>,
|
||||||
hooks: Vec<Rc<RefCell<dyn std::any::Any>>>,
|
}
|
||||||
destroy_listeners: Vec<Box<dyn FnOnce()>>,
|
|
||||||
|
impl HookContext {
|
||||||
|
pub(crate) fn next_state<T>(&mut self, initializer: impl FnOnce(ReRender) -> T) -> Rc<T>
|
||||||
|
where
|
||||||
|
T: 'static,
|
||||||
|
{
|
||||||
|
// Determine which hook position we're at and increment for the next hook
|
||||||
|
let hook_pos = self.counter;
|
||||||
|
self.counter += 1;
|
||||||
|
|
||||||
|
let state = match self.states.get(hook_pos).cloned() {
|
||||||
|
Some(m) => m,
|
||||||
|
None => {
|
||||||
|
let initial_state = Rc::new(initializer(self.re_render.clone()));
|
||||||
|
self.states.push(initial_state.clone());
|
||||||
|
|
||||||
|
initial_state
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
state.downcast().unwrap()
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(crate) fn next_effect<T>(&mut self, initializer: impl FnOnce(ReRender) -> T) -> Rc<T>
|
||||||
|
where
|
||||||
|
T: 'static + Effect,
|
||||||
|
{
|
||||||
|
let prev_state_len = self.states.len();
|
||||||
|
let t = self.next_state(initializer);
|
||||||
|
|
||||||
|
// This is a new effect, we add it to effects.
|
||||||
|
if self.states.len() != prev_state_len {
|
||||||
|
self.effects.push(t.clone());
|
||||||
|
}
|
||||||
|
|
||||||
|
t
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl fmt::Debug for HookContext {
|
||||||
|
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||||
|
f.write_str("HookContext<_>")
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Trait that allows a struct to act as Function Component.
|
/// Trait that allows a struct to act as Function Component.
|
||||||
@ -76,14 +130,13 @@ pub trait FunctionProvider {
|
|||||||
/// Render the component. This function returns the [`Html`](crate::Html) to be rendered for the component.
|
/// Render the component. This function returns the [`Html`](crate::Html) to be rendered for the component.
|
||||||
///
|
///
|
||||||
/// Equivalent of [`Component::view`](crate::html::Component::view).
|
/// Equivalent of [`Component::view`](crate::html::Component::view).
|
||||||
fn run(props: &Self::TProps) -> HtmlResult;
|
fn run(ctx: &mut HookContext, props: &Self::TProps) -> HtmlResult;
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Wrapper that allows a struct implementing [`FunctionProvider`] to be consumed as a component.
|
/// Wrapper that allows a struct implementing [`FunctionProvider`] to be consumed as a component.
|
||||||
pub struct FunctionComponent<T: FunctionProvider + 'static> {
|
pub struct FunctionComponent<T: FunctionProvider + 'static> {
|
||||||
_never: std::marker::PhantomData<T>,
|
_never: std::marker::PhantomData<T>,
|
||||||
hook_state: RefCell<HookState>,
|
hook_ctx: RefCell<HookContext>,
|
||||||
message_queue: MsgQueue,
|
|
||||||
}
|
}
|
||||||
|
|
||||||
impl<T: FunctionProvider> fmt::Debug for FunctionComponent<T> {
|
impl<T: FunctionProvider> fmt::Debug for FunctionComponent<T> {
|
||||||
@ -92,52 +145,36 @@ impl<T: FunctionProvider> fmt::Debug for FunctionComponent<T> {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl<T> FunctionComponent<T>
|
|
||||||
where
|
|
||||||
T: FunctionProvider,
|
|
||||||
{
|
|
||||||
fn with_hook_state<R>(&self, f: impl FnOnce() -> R) -> R {
|
|
||||||
let mut hook_state = self.hook_state.borrow_mut();
|
|
||||||
hook_state.counter = 0;
|
|
||||||
CURRENT_HOOK.set(&mut *hook_state, f)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl<T> BaseComponent for FunctionComponent<T>
|
impl<T> BaseComponent for FunctionComponent<T>
|
||||||
where
|
where
|
||||||
T: FunctionProvider + 'static,
|
T: FunctionProvider + 'static,
|
||||||
{
|
{
|
||||||
type Message = Box<dyn FnOnce() -> bool>;
|
type Message = ();
|
||||||
type Properties = T::TProps;
|
type Properties = T::TProps;
|
||||||
|
|
||||||
fn create(ctx: &Context<Self>) -> Self {
|
fn create(ctx: &Context<Self>) -> Self {
|
||||||
let scope = AnyScope::from(ctx.link().clone());
|
let scope = AnyScope::from(ctx.link().clone());
|
||||||
let message_queue = MsgQueue::default();
|
|
||||||
|
|
||||||
Self {
|
Self {
|
||||||
_never: std::marker::PhantomData::default(),
|
_never: std::marker::PhantomData::default(),
|
||||||
message_queue: message_queue.clone(),
|
hook_ctx: RefCell::new(HookContext {
|
||||||
hook_state: RefCell::new(HookState {
|
effects: Vec::new(),
|
||||||
counter: 0,
|
|
||||||
scope,
|
scope,
|
||||||
process_message: {
|
re_render: {
|
||||||
let scope = ctx.link().clone();
|
let link = ctx.link().clone();
|
||||||
Rc::new(move |msg, post_render| {
|
Rc::new(move || link.send_message(()))
|
||||||
if post_render {
|
|
||||||
message_queue.push(msg);
|
|
||||||
} else {
|
|
||||||
scope.send_message(msg);
|
|
||||||
}
|
|
||||||
})
|
|
||||||
},
|
},
|
||||||
hooks: vec![],
|
states: Vec::new(),
|
||||||
destroy_listeners: vec![],
|
|
||||||
|
counter: 0,
|
||||||
|
#[cfg(debug_assertions)]
|
||||||
|
total_hook_counter: None,
|
||||||
}),
|
}),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn update(&mut self, _ctx: &Context<Self>, msg: Self::Message) -> bool {
|
fn update(&mut self, _ctx: &Context<Self>, _msg: Self::Message) -> bool {
|
||||||
msg()
|
true
|
||||||
}
|
}
|
||||||
|
|
||||||
fn changed(&mut self, _ctx: &Context<Self>) -> bool {
|
fn changed(&mut self, _ctx: &Context<Self>) -> bool {
|
||||||
@ -145,107 +182,58 @@ where
|
|||||||
}
|
}
|
||||||
|
|
||||||
fn view(&self, ctx: &Context<Self>) -> HtmlResult {
|
fn view(&self, ctx: &Context<Self>) -> HtmlResult {
|
||||||
self.with_hook_state(|| T::run(&*ctx.props()))
|
let props = ctx.props();
|
||||||
|
let mut ctx = self.hook_ctx.borrow_mut();
|
||||||
|
ctx.counter = 0;
|
||||||
|
|
||||||
|
#[allow(clippy::let_and_return)]
|
||||||
|
let result = T::run(&mut *ctx, props);
|
||||||
|
|
||||||
|
#[cfg(debug_assertions)]
|
||||||
|
{
|
||||||
|
// Procedural Macros can catch most conditionally called hooks at compile time, but it cannot
|
||||||
|
// detect early return (as the return can be Err(_), Suspension).
|
||||||
|
if result.is_err() {
|
||||||
|
if let Some(m) = ctx.total_hook_counter {
|
||||||
|
// Suspended Components can have less hooks called when suspended, but not more.
|
||||||
|
if m < ctx.counter {
|
||||||
|
panic!("Hooks are called conditionally.");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
match ctx.total_hook_counter {
|
||||||
|
Some(m) => {
|
||||||
|
if m != ctx.counter {
|
||||||
|
panic!("Hooks are called conditionally.");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
None => {
|
||||||
|
ctx.total_hook_counter = Some(ctx.counter);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
result
|
||||||
}
|
}
|
||||||
|
|
||||||
fn rendered(&mut self, ctx: &Context<Self>, _first_render: bool) {
|
fn rendered(&mut self, _ctx: &Context<Self>, _first_render: bool) {
|
||||||
for msg in self.message_queue.drain() {
|
let hook_ctx = self.hook_ctx.borrow();
|
||||||
ctx.link().send_message(msg);
|
|
||||||
|
for effect in hook_ctx.effects.iter() {
|
||||||
|
effect.rendered();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn destroy(&mut self, _ctx: &Context<Self>) {
|
fn destroy(&mut self, _ctx: &Context<Self>) {
|
||||||
let mut hook_state = self.hook_state.borrow_mut();
|
let mut hook_ctx = self.hook_ctx.borrow_mut();
|
||||||
for hook in hook_state.destroy_listeners.drain(..) {
|
// We clear the effects as these are also references to states.
|
||||||
hook()
|
hook_ctx.effects.clear();
|
||||||
|
|
||||||
|
for state in hook_ctx.states.drain(..) {
|
||||||
|
drop(state);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub(crate) fn get_current_scope() -> Option<AnyScope> {
|
|
||||||
if CURRENT_HOOK.is_set() {
|
|
||||||
Some(CURRENT_HOOK.with(|state| state.scope.clone()))
|
|
||||||
} else {
|
|
||||||
None
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl<T> SealedBaseComponent for FunctionComponent<T> where T: FunctionProvider + 'static {}
|
impl<T> SealedBaseComponent for FunctionComponent<T> where T: FunctionProvider + 'static {}
|
||||||
|
|
||||||
#[derive(Clone, Default)]
|
|
||||||
struct MsgQueue(Rc<RefCell<Vec<Msg>>>);
|
|
||||||
|
|
||||||
impl MsgQueue {
|
|
||||||
fn push(&self, msg: Msg) {
|
|
||||||
self.0.borrow_mut().push(msg);
|
|
||||||
}
|
|
||||||
|
|
||||||
fn drain(&self) -> Vec<Msg> {
|
|
||||||
self.0.borrow_mut().drain(..).collect()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// The `HookUpdater` provides a convenient interface for hooking into the lifecycle of
|
|
||||||
/// the underlying Yew Component that backs the function component.
|
|
||||||
///
|
|
||||||
/// Two interfaces are provided - callback and post_render.
|
|
||||||
/// - `callback` allows the creation of regular yew callbacks on the host component.
|
|
||||||
/// - `post_render` allows the creation of events that happen after a render is complete.
|
|
||||||
///
|
|
||||||
/// See [`use_effect`](hooks::use_effect()) and [`use_context`](hooks::use_context())
|
|
||||||
/// for more details on how to use the hook updater to provide function components
|
|
||||||
/// the necessary callbacks to update the underlying state.
|
|
||||||
#[derive(Clone)]
|
|
||||||
#[allow(missing_debug_implementations)]
|
|
||||||
pub struct HookUpdater {
|
|
||||||
hook: Rc<RefCell<dyn std::any::Any>>,
|
|
||||||
process_message: ProcessMessage,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl HookUpdater {
|
|
||||||
/// Callback which runs the hook.
|
|
||||||
pub fn callback<T: 'static, F>(&self, cb: F)
|
|
||||||
where
|
|
||||||
F: FnOnce(&mut T) -> bool + 'static,
|
|
||||||
{
|
|
||||||
let internal_hook_state = self.hook.clone();
|
|
||||||
let process_message = self.process_message.clone();
|
|
||||||
|
|
||||||
// Update the component
|
|
||||||
// We're calling "link.send_message", so we're not calling it post-render
|
|
||||||
let post_render = false;
|
|
||||||
process_message(
|
|
||||||
Box::new(move || {
|
|
||||||
let mut r = internal_hook_state.borrow_mut();
|
|
||||||
let hook: &mut T = r
|
|
||||||
.downcast_mut()
|
|
||||||
.expect("internal error: hook downcasted to wrong type");
|
|
||||||
cb(hook)
|
|
||||||
}),
|
|
||||||
post_render,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Callback called after the render
|
|
||||||
pub fn post_render<T: 'static, F>(&self, cb: F)
|
|
||||||
where
|
|
||||||
F: FnOnce(&mut T) -> bool + 'static,
|
|
||||||
{
|
|
||||||
let internal_hook_state = self.hook.clone();
|
|
||||||
let process_message = self.process_message.clone();
|
|
||||||
|
|
||||||
// Update the component
|
|
||||||
// We're calling "message_queue.push", so not calling it post-render
|
|
||||||
let post_render = true;
|
|
||||||
process_message(
|
|
||||||
Box::new(move || {
|
|
||||||
let mut hook = internal_hook_state.borrow_mut();
|
|
||||||
let hook: &mut T = hook
|
|
||||||
.downcast_mut()
|
|
||||||
.expect("internal error: hook downcasted to wrong type");
|
|
||||||
cb(hook)
|
|
||||||
}),
|
|
||||||
post_render,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|||||||
@ -206,6 +206,7 @@ mod ssr_tests {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[hook]
|
||||||
pub fn use_sleep() -> SuspensionResult<Rc<dyn Fn()>> {
|
pub fn use_sleep() -> SuspensionResult<Rc<dyn Fn()>> {
|
||||||
let sleep_state = use_reducer(SleepState::new);
|
let sleep_state = use_reducer(SleepState::new);
|
||||||
|
|
||||||
|
|||||||
@ -2,31 +2,27 @@ mod common;
|
|||||||
|
|
||||||
use common::obtain_result;
|
use common::obtain_result;
|
||||||
use wasm_bindgen_test::*;
|
use wasm_bindgen_test::*;
|
||||||
use yew::functional::{FunctionComponent, FunctionProvider};
|
use yew::prelude::*;
|
||||||
use yew::{html, HtmlResult, Properties};
|
|
||||||
|
|
||||||
wasm_bindgen_test::wasm_bindgen_test_configure!(run_in_browser);
|
wasm_bindgen_test::wasm_bindgen_test_configure!(run_in_browser);
|
||||||
|
|
||||||
#[wasm_bindgen_test]
|
#[wasm_bindgen_test]
|
||||||
fn props_are_passed() {
|
fn props_are_passed() {
|
||||||
struct PropsPassedFunction {}
|
|
||||||
#[derive(Properties, Clone, PartialEq)]
|
#[derive(Properties, Clone, PartialEq)]
|
||||||
struct PropsPassedFunctionProps {
|
struct PropsPassedFunctionProps {
|
||||||
value: String,
|
value: String,
|
||||||
}
|
}
|
||||||
impl FunctionProvider for PropsPassedFunction {
|
|
||||||
type TProps = PropsPassedFunctionProps;
|
|
||||||
|
|
||||||
fn run(props: &Self::TProps) -> HtmlResult {
|
#[function_component]
|
||||||
assert_eq!(&props.value, "props");
|
fn PropsComponent(props: &PropsPassedFunctionProps) -> Html {
|
||||||
return Ok(html! {
|
assert_eq!(&props.value, "props");
|
||||||
<div id="result">
|
html! {
|
||||||
{"done"}
|
<div id="result">
|
||||||
</div>
|
{"done"}
|
||||||
});
|
</div>
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
type PropsComponent = FunctionComponent<PropsPassedFunction>;
|
|
||||||
yew::start_app_with_props_in_element::<PropsComponent>(
|
yew::start_app_with_props_in_element::<PropsComponent>(
|
||||||
gloo_utils::document().get_element_by_id("output").unwrap(),
|
gloo_utils::document().get_element_by_id("output").unwrap(),
|
||||||
PropsPassedFunctionProps {
|
PropsPassedFunctionProps {
|
||||||
|
|||||||
@ -44,6 +44,7 @@ async fn suspense_works() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[hook]
|
||||||
pub fn use_sleep() -> SuspensionResult<Rc<dyn Fn()>> {
|
pub fn use_sleep() -> SuspensionResult<Rc<dyn Fn()>> {
|
||||||
let sleep_state = use_reducer(SleepState::new);
|
let sleep_state = use_reducer(SleepState::new);
|
||||||
|
|
||||||
@ -182,6 +183,7 @@ async fn suspense_not_suspended_at_start() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[hook]
|
||||||
pub fn use_sleep() -> SuspensionResult<Rc<dyn Fn()>> {
|
pub fn use_sleep() -> SuspensionResult<Rc<dyn Fn()>> {
|
||||||
let sleep_state = use_reducer(SleepState::new);
|
let sleep_state = use_reducer(SleepState::new);
|
||||||
|
|
||||||
@ -297,6 +299,7 @@ async fn suspense_nested_suspense_works() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[hook]
|
||||||
pub fn use_sleep() -> SuspensionResult<Rc<dyn Fn()>> {
|
pub fn use_sleep() -> SuspensionResult<Rc<dyn Fn()>> {
|
||||||
let sleep_state = use_reducer(SleepState::new);
|
let sleep_state = use_reducer(SleepState::new);
|
||||||
|
|
||||||
@ -430,6 +433,7 @@ async fn effects_not_run_when_suspended() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[hook]
|
||||||
pub fn use_sleep() -> SuspensionResult<Rc<dyn Fn()>> {
|
pub fn use_sleep() -> SuspensionResult<Rc<dyn Fn()>> {
|
||||||
let sleep_state = use_reducer(SleepState::new);
|
let sleep_state = use_reducer(SleepState::new);
|
||||||
|
|
||||||
|
|||||||
@ -3,10 +3,7 @@ mod common;
|
|||||||
use common::obtain_result_by_id;
|
use common::obtain_result_by_id;
|
||||||
use std::rc::Rc;
|
use std::rc::Rc;
|
||||||
use wasm_bindgen_test::*;
|
use wasm_bindgen_test::*;
|
||||||
use yew::functional::{
|
use yew::prelude::*;
|
||||||
use_context, use_effect, use_mut_ref, use_state, FunctionComponent, FunctionProvider,
|
|
||||||
};
|
|
||||||
use yew::{html, Children, ContextProvider, HtmlResult, Properties};
|
|
||||||
|
|
||||||
wasm_bindgen_test::wasm_bindgen_test_configure!(run_in_browser);
|
wasm_bindgen_test::wasm_bindgen_test_configure!(run_in_browser);
|
||||||
|
|
||||||
@ -14,61 +11,51 @@ wasm_bindgen_test::wasm_bindgen_test_configure!(run_in_browser);
|
|||||||
fn use_context_scoping_works() {
|
fn use_context_scoping_works() {
|
||||||
#[derive(Clone, Debug, PartialEq)]
|
#[derive(Clone, Debug, PartialEq)]
|
||||||
struct ExampleContext(String);
|
struct ExampleContext(String);
|
||||||
struct UseContextFunctionOuter {}
|
|
||||||
struct UseContextFunctionInner {}
|
|
||||||
struct ExpectNoContextFunction {}
|
|
||||||
type UseContextComponent = FunctionComponent<UseContextFunctionOuter>;
|
|
||||||
type UseContextComponentInner = FunctionComponent<UseContextFunctionInner>;
|
|
||||||
type ExpectNoContextComponent = FunctionComponent<ExpectNoContextFunction>;
|
|
||||||
impl FunctionProvider for ExpectNoContextFunction {
|
|
||||||
type TProps = ();
|
|
||||||
|
|
||||||
fn run(_props: &Self::TProps) -> HtmlResult {
|
#[function_component]
|
||||||
if use_context::<ExampleContext>().is_some() {
|
fn ExpectNoContextComponent() -> Html {
|
||||||
console_log!(
|
let example_context = use_context::<ExampleContext>();
|
||||||
"Context should be None here, but was {:?}!",
|
|
||||||
use_context::<ExampleContext>().unwrap()
|
if example_context.is_some() {
|
||||||
);
|
console_log!(
|
||||||
};
|
"Context should be None here, but was {:?}!",
|
||||||
Ok(html! {
|
example_context
|
||||||
<div></div>
|
);
|
||||||
})
|
};
|
||||||
|
html! {
|
||||||
|
<div></div>
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
impl FunctionProvider for UseContextFunctionOuter {
|
|
||||||
type TProps = ();
|
|
||||||
|
|
||||||
fn run(_props: &Self::TProps) -> HtmlResult {
|
#[function_component]
|
||||||
type ExampleContextProvider = ContextProvider<ExampleContext>;
|
fn UseContextComponent() -> Html {
|
||||||
Ok(html! {
|
type ExampleContextProvider = ContextProvider<ExampleContext>;
|
||||||
<div>
|
html! {
|
||||||
<ExampleContextProvider context={ExampleContext("wrong1".into())}>
|
<div>
|
||||||
<div>{"ignored"}</div>
|
<ExampleContextProvider context={ExampleContext("wrong1".into())}>
|
||||||
</ExampleContextProvider>
|
<div>{"ignored"}</div>
|
||||||
<ExampleContextProvider context={ExampleContext("wrong2".into())}>
|
</ExampleContextProvider>
|
||||||
<ExampleContextProvider context={ExampleContext("correct".into())}>
|
<ExampleContextProvider context={ExampleContext("wrong2".into())}>
|
||||||
<ExampleContextProvider context={ExampleContext("wrong1".into())}>
|
<ExampleContextProvider context={ExampleContext("correct".into())}>
|
||||||
<div>{"ignored"}</div>
|
<ExampleContextProvider context={ExampleContext("wrong1".into())}>
|
||||||
</ExampleContextProvider>
|
<div>{"ignored"}</div>
|
||||||
<UseContextComponentInner />
|
|
||||||
</ExampleContextProvider>
|
</ExampleContextProvider>
|
||||||
|
<UseContextComponentInner />
|
||||||
</ExampleContextProvider>
|
</ExampleContextProvider>
|
||||||
<ExampleContextProvider context={ExampleContext("wrong3".into())}>
|
</ExampleContextProvider>
|
||||||
<div>{"ignored"}</div>
|
<ExampleContextProvider context={ExampleContext("wrong3".into())}>
|
||||||
</ExampleContextProvider>
|
<div>{"ignored"}</div>
|
||||||
<ExpectNoContextComponent />
|
</ExampleContextProvider>
|
||||||
</div>
|
<ExpectNoContextComponent />
|
||||||
})
|
</div>
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
impl FunctionProvider for UseContextFunctionInner {
|
|
||||||
type TProps = ();
|
|
||||||
|
|
||||||
fn run(_props: &Self::TProps) -> HtmlResult {
|
#[function_component]
|
||||||
let context = use_context::<ExampleContext>();
|
fn UseContextComponentInner() -> Html {
|
||||||
Ok(html! {
|
let context = use_context::<ExampleContext>();
|
||||||
<div id="result">{ &context.unwrap().0 }</div>
|
html! {
|
||||||
})
|
<div id="result">{ &context.unwrap().0 }</div>
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -86,83 +73,70 @@ fn use_context_works_with_multiple_types() {
|
|||||||
#[derive(Clone, Debug, PartialEq)]
|
#[derive(Clone, Debug, PartialEq)]
|
||||||
struct ContextB(u32);
|
struct ContextB(u32);
|
||||||
|
|
||||||
struct Test1Function;
|
#[function_component]
|
||||||
impl FunctionProvider for Test1Function {
|
fn Test1() -> Html {
|
||||||
type TProps = ();
|
let ctx_a = use_context::<ContextA>();
|
||||||
|
let ctx_b = use_context::<ContextB>();
|
||||||
|
|
||||||
fn run(_props: &Self::TProps) -> HtmlResult {
|
assert_eq!(ctx_a, Some(ContextA(2)));
|
||||||
assert_eq!(use_context::<ContextA>(), Some(ContextA(2)));
|
assert_eq!(ctx_b, Some(ContextB(1)));
|
||||||
assert_eq!(use_context::<ContextB>(), Some(ContextB(1)));
|
|
||||||
|
|
||||||
Ok(html! {})
|
html! {}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[function_component]
|
||||||
|
fn Test2() -> Html {
|
||||||
|
let ctx_a = use_context::<ContextA>();
|
||||||
|
let ctx_b = use_context::<ContextB>();
|
||||||
|
|
||||||
|
assert_eq!(ctx_a, Some(ContextA(0)));
|
||||||
|
assert_eq!(ctx_b, Some(ContextB(1)));
|
||||||
|
|
||||||
|
html! {}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[function_component]
|
||||||
|
fn Test3() -> Html {
|
||||||
|
let ctx_a = use_context::<ContextA>();
|
||||||
|
let ctx_b = use_context::<ContextB>();
|
||||||
|
|
||||||
|
assert_eq!(ctx_a, Some(ContextA(0)));
|
||||||
|
assert_eq!(ctx_b, None);
|
||||||
|
|
||||||
|
html! {}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[function_component]
|
||||||
|
fn Test4() -> Html {
|
||||||
|
let ctx_a = use_context::<ContextA>();
|
||||||
|
let ctx_b = use_context::<ContextB>();
|
||||||
|
|
||||||
|
assert_eq!(ctx_a, None);
|
||||||
|
assert_eq!(ctx_b, None);
|
||||||
|
|
||||||
|
html! {}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[function_component]
|
||||||
|
fn TestComponent() -> Html {
|
||||||
|
type ContextAProvider = ContextProvider<ContextA>;
|
||||||
|
type ContextBProvider = ContextProvider<ContextB>;
|
||||||
|
|
||||||
|
html! {
|
||||||
|
<div>
|
||||||
|
<ContextAProvider context={ContextA(0)}>
|
||||||
|
<ContextBProvider context={ContextB(1)}>
|
||||||
|
<ContextAProvider context={ContextA(2)}>
|
||||||
|
<Test1/>
|
||||||
|
</ContextAProvider>
|
||||||
|
<Test2/>
|
||||||
|
</ContextBProvider>
|
||||||
|
<Test3/>
|
||||||
|
</ContextAProvider>
|
||||||
|
<Test4 />
|
||||||
|
</div>
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
type Test1 = FunctionComponent<Test1Function>;
|
|
||||||
|
|
||||||
struct Test2Function;
|
|
||||||
impl FunctionProvider for Test2Function {
|
|
||||||
type TProps = ();
|
|
||||||
|
|
||||||
fn run(_props: &Self::TProps) -> HtmlResult {
|
|
||||||
assert_eq!(use_context::<ContextA>(), Some(ContextA(0)));
|
|
||||||
assert_eq!(use_context::<ContextB>(), Some(ContextB(1)));
|
|
||||||
|
|
||||||
Ok(html! {})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
type Test2 = FunctionComponent<Test2Function>;
|
|
||||||
|
|
||||||
struct Test3Function;
|
|
||||||
impl FunctionProvider for Test3Function {
|
|
||||||
type TProps = ();
|
|
||||||
|
|
||||||
fn run(_props: &Self::TProps) -> HtmlResult {
|
|
||||||
assert_eq!(use_context::<ContextA>(), Some(ContextA(0)));
|
|
||||||
assert_eq!(use_context::<ContextB>(), None);
|
|
||||||
|
|
||||||
Ok(html! {})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
type Test3 = FunctionComponent<Test3Function>;
|
|
||||||
|
|
||||||
struct Test4Function;
|
|
||||||
impl FunctionProvider for Test4Function {
|
|
||||||
type TProps = ();
|
|
||||||
|
|
||||||
fn run(_props: &Self::TProps) -> HtmlResult {
|
|
||||||
assert_eq!(use_context::<ContextA>(), None);
|
|
||||||
assert_eq!(use_context::<ContextB>(), None);
|
|
||||||
|
|
||||||
Ok(html! {})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
type Test4 = FunctionComponent<Test4Function>;
|
|
||||||
|
|
||||||
struct TestFunction;
|
|
||||||
impl FunctionProvider for TestFunction {
|
|
||||||
type TProps = ();
|
|
||||||
|
|
||||||
fn run(_props: &Self::TProps) -> HtmlResult {
|
|
||||||
type ContextAProvider = ContextProvider<ContextA>;
|
|
||||||
type ContextBProvider = ContextProvider<ContextB>;
|
|
||||||
|
|
||||||
Ok(html! {
|
|
||||||
<div>
|
|
||||||
<ContextAProvider context={ContextA(0)}>
|
|
||||||
<ContextBProvider context={ContextB(1)}>
|
|
||||||
<ContextAProvider context={ContextA(2)}>
|
|
||||||
<Test1/>
|
|
||||||
</ContextAProvider>
|
|
||||||
<Test2/>
|
|
||||||
</ContextBProvider>
|
|
||||||
<Test3/>
|
|
||||||
</ContextAProvider>
|
|
||||||
<Test4 />
|
|
||||||
</div>
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
type TestComponent = FunctionComponent<TestFunction>;
|
|
||||||
|
|
||||||
yew::start_app_in_element::<TestComponent>(
|
yew::start_app_in_element::<TestComponent>(
|
||||||
gloo_utils::document().get_element_by_id("output").unwrap(),
|
gloo_utils::document().get_element_by_id("output").unwrap(),
|
||||||
@ -180,24 +154,19 @@ fn use_context_update_works() {
|
|||||||
children: Children,
|
children: Children,
|
||||||
}
|
}
|
||||||
|
|
||||||
struct RenderCounterFunction;
|
#[function_component]
|
||||||
impl FunctionProvider for RenderCounterFunction {
|
fn RenderCounter(props: &RenderCounterProps) -> Html {
|
||||||
type TProps = RenderCounterProps;
|
let counter = use_mut_ref(|| 0);
|
||||||
|
*counter.borrow_mut() += 1;
|
||||||
fn run(props: &Self::TProps) -> HtmlResult {
|
html! {
|
||||||
let counter = use_mut_ref(|| 0);
|
<>
|
||||||
*counter.borrow_mut() += 1;
|
<div id={props.id.clone()}>
|
||||||
Ok(html! {
|
{ format!("total: {}", counter.borrow()) }
|
||||||
<>
|
</div>
|
||||||
<div id={props.id.clone()}>
|
{ props.children.clone() }
|
||||||
{ format!("total: {}", counter.borrow()) }
|
</>
|
||||||
</div>
|
|
||||||
{ props.children.clone() }
|
|
||||||
</>
|
|
||||||
})
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
type RenderCounter = FunctionComponent<RenderCounterFunction>;
|
|
||||||
|
|
||||||
#[derive(Clone, Debug, PartialEq, Properties)]
|
#[derive(Clone, Debug, PartialEq, Properties)]
|
||||||
struct ContextOutletProps {
|
struct ContextOutletProps {
|
||||||
@ -205,75 +174,66 @@ fn use_context_update_works() {
|
|||||||
#[prop_or_default]
|
#[prop_or_default]
|
||||||
magic: usize,
|
magic: usize,
|
||||||
}
|
}
|
||||||
struct ContextOutletFunction;
|
|
||||||
impl FunctionProvider for ContextOutletFunction {
|
|
||||||
type TProps = ContextOutletProps;
|
|
||||||
|
|
||||||
fn run(props: &Self::TProps) -> HtmlResult {
|
#[function_component]
|
||||||
let counter = use_mut_ref(|| 0);
|
fn ContextOutlet(props: &ContextOutletProps) -> Html {
|
||||||
*counter.borrow_mut() += 1;
|
let counter = use_mut_ref(|| 0);
|
||||||
|
*counter.borrow_mut() += 1;
|
||||||
|
|
||||||
let ctx = use_context::<Rc<MyContext>>().expect("context not passed down");
|
let ctx = use_context::<Rc<MyContext>>().expect("context not passed down");
|
||||||
|
|
||||||
Ok(html! {
|
html! {
|
||||||
<>
|
<>
|
||||||
<div>{ format!("magic: {}\n", props.magic) }</div>
|
<div>{ format!("magic: {}\n", props.magic) }</div>
|
||||||
<div id={props.id.clone()}>
|
<div id={props.id.clone()}>
|
||||||
{ format!("current: {}, total: {}", ctx.0, counter.borrow()) }
|
{ format!("current: {}, total: {}", ctx.0, counter.borrow()) }
|
||||||
</div>
|
</div>
|
||||||
</>
|
</>
|
||||||
})
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
type ContextOutlet = FunctionComponent<ContextOutletFunction>;
|
|
||||||
|
|
||||||
struct TestFunction;
|
#[function_component]
|
||||||
impl FunctionProvider for TestFunction {
|
fn TestComponent() -> Html {
|
||||||
type TProps = ();
|
type MyContextProvider = ContextProvider<Rc<MyContext>>;
|
||||||
|
|
||||||
fn run(_props: &Self::TProps) -> HtmlResult {
|
let ctx = use_state(|| MyContext("hello".into()));
|
||||||
type MyContextProvider = ContextProvider<Rc<MyContext>>;
|
let rendered = use_mut_ref(|| 0);
|
||||||
|
|
||||||
let ctx = use_state(|| MyContext("hello".into()));
|
// this is used to force an update specific to test-2
|
||||||
let rendered = use_mut_ref(|| 0);
|
let magic_rc = use_state(|| 0);
|
||||||
|
let magic: usize = *magic_rc;
|
||||||
// this is used to force an update specific to test-2
|
{
|
||||||
let magic_rc = use_state(|| 0);
|
let ctx = ctx.clone();
|
||||||
let magic: usize = *magic_rc;
|
use_effect(move || {
|
||||||
{
|
let count = *rendered.borrow();
|
||||||
let ctx = ctx.clone();
|
match count {
|
||||||
use_effect(move || {
|
0 => {
|
||||||
let count = *rendered.borrow();
|
ctx.set(MyContext("world".into()));
|
||||||
match count {
|
*rendered.borrow_mut() += 1;
|
||||||
0 => {
|
}
|
||||||
ctx.set(MyContext("world".into()));
|
1 => {
|
||||||
*rendered.borrow_mut() += 1;
|
// force test-2 to re-render.
|
||||||
}
|
magic_rc.set(1);
|
||||||
1 => {
|
*rendered.borrow_mut() += 1;
|
||||||
// force test-2 to re-render.
|
}
|
||||||
magic_rc.set(1);
|
2 => {
|
||||||
*rendered.borrow_mut() += 1;
|
ctx.set(MyContext("hello world!".into()));
|
||||||
}
|
*rendered.borrow_mut() += 1;
|
||||||
2 => {
|
}
|
||||||
ctx.set(MyContext("hello world!".into()));
|
_ => (),
|
||||||
*rendered.borrow_mut() += 1;
|
};
|
||||||
}
|
|| {}
|
||||||
_ => (),
|
});
|
||||||
};
|
}
|
||||||
|| {}
|
html! {
|
||||||
});
|
<MyContextProvider context={Rc::new((*ctx).clone())}>
|
||||||
}
|
<RenderCounter id="test-0">
|
||||||
Ok(html! {
|
<ContextOutlet id="test-1"/>
|
||||||
<MyContextProvider context={Rc::new((*ctx).clone())}>
|
<ContextOutlet id="test-2" {magic}/>
|
||||||
<RenderCounter id="test-0">
|
</RenderCounter>
|
||||||
<ContextOutlet id="test-1"/>
|
</MyContextProvider>
|
||||||
<ContextOutlet id="test-2" {magic}/>
|
|
||||||
</RenderCounter>
|
|
||||||
</MyContextProvider>
|
|
||||||
})
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
type TestComponent = FunctionComponent<TestFunction>;
|
|
||||||
|
|
||||||
yew::start_app_in_element::<TestComponent>(
|
yew::start_app_in_element::<TestComponent>(
|
||||||
gloo_utils::document().get_element_by_id("output").unwrap(),
|
gloo_utils::document().get_element_by_id("output").unwrap(),
|
||||||
|
|||||||
@ -4,17 +4,12 @@ use common::obtain_result;
|
|||||||
use std::ops::{Deref, DerefMut};
|
use std::ops::{Deref, DerefMut};
|
||||||
use std::rc::Rc;
|
use std::rc::Rc;
|
||||||
use wasm_bindgen_test::*;
|
use wasm_bindgen_test::*;
|
||||||
use yew::functional::{
|
use yew::prelude::*;
|
||||||
use_effect_with_deps, use_mut_ref, use_state, FunctionComponent, FunctionProvider,
|
|
||||||
};
|
|
||||||
use yew::{html, HtmlResult, Properties};
|
|
||||||
|
|
||||||
wasm_bindgen_test::wasm_bindgen_test_configure!(run_in_browser);
|
wasm_bindgen_test::wasm_bindgen_test_configure!(run_in_browser);
|
||||||
|
|
||||||
#[wasm_bindgen_test]
|
#[wasm_bindgen_test]
|
||||||
fn use_effect_destroys_on_component_drop() {
|
fn use_effect_destroys_on_component_drop() {
|
||||||
struct UseEffectFunction {}
|
|
||||||
struct UseEffectWrapper {}
|
|
||||||
#[derive(Properties, Clone)]
|
#[derive(Properties, Clone)]
|
||||||
struct WrapperProps {
|
struct WrapperProps {
|
||||||
destroy_called: Rc<dyn Fn()>,
|
destroy_called: Rc<dyn Fn()>,
|
||||||
@ -34,42 +29,37 @@ fn use_effect_destroys_on_component_drop() {
|
|||||||
false
|
false
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
type UseEffectComponent = FunctionComponent<UseEffectFunction>;
|
|
||||||
type UseEffectWrapperComponent = FunctionComponent<UseEffectWrapper>;
|
|
||||||
impl FunctionProvider for UseEffectFunction {
|
|
||||||
type TProps = FunctionProps;
|
|
||||||
|
|
||||||
fn run(props: &Self::TProps) -> HtmlResult {
|
#[function_component(UseEffectComponent)]
|
||||||
let effect_called = props.effect_called.clone();
|
fn use_effect_comp(props: &FunctionProps) -> Html {
|
||||||
let destroy_called = props.destroy_called.clone();
|
let effect_called = props.effect_called.clone();
|
||||||
use_effect_with_deps(
|
let destroy_called = props.destroy_called.clone();
|
||||||
move |_| {
|
use_effect_with_deps(
|
||||||
effect_called();
|
move |_| {
|
||||||
#[allow(clippy::redundant_closure)] // Otherwise there is a build error
|
effect_called();
|
||||||
move || destroy_called()
|
#[allow(clippy::redundant_closure)] // Otherwise there is a build error
|
||||||
},
|
move || destroy_called()
|
||||||
(),
|
},
|
||||||
);
|
(),
|
||||||
Ok(html! {})
|
);
|
||||||
}
|
html! {}
|
||||||
}
|
}
|
||||||
impl FunctionProvider for UseEffectWrapper {
|
|
||||||
type TProps = WrapperProps;
|
|
||||||
|
|
||||||
fn run(props: &Self::TProps) -> HtmlResult {
|
#[function_component(UseEffectWrapperComponent)]
|
||||||
let show = use_state(|| true);
|
fn use_effect_wrapper_comp(props: &WrapperProps) -> Html {
|
||||||
if *show {
|
let show = use_state(|| true);
|
||||||
let effect_called: Rc<dyn Fn()> = { Rc::new(move || show.set(false)) };
|
if *show {
|
||||||
Ok(html! {
|
let effect_called: Rc<dyn Fn()> = { Rc::new(move || show.set(false)) };
|
||||||
<UseEffectComponent destroy_called={props.destroy_called.clone()} {effect_called} />
|
html! {
|
||||||
})
|
<UseEffectComponent destroy_called={props.destroy_called.clone()} {effect_called} />
|
||||||
} else {
|
}
|
||||||
Ok(html! {
|
} else {
|
||||||
<div>{ "EMPTY" }</div>
|
html! {
|
||||||
})
|
<div>{ "EMPTY" }</div>
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
let destroy_counter = Rc::new(std::cell::RefCell::new(0));
|
let destroy_counter = Rc::new(std::cell::RefCell::new(0));
|
||||||
let destroy_counter_c = destroy_counter.clone();
|
let destroy_counter_c = destroy_counter.clone();
|
||||||
yew::start_app_with_props_in_element::<UseEffectWrapperComponent>(
|
yew::start_app_with_props_in_element::<UseEffectWrapperComponent>(
|
||||||
@ -83,35 +73,30 @@ fn use_effect_destroys_on_component_drop() {
|
|||||||
|
|
||||||
#[wasm_bindgen_test]
|
#[wasm_bindgen_test]
|
||||||
fn use_effect_works_many_times() {
|
fn use_effect_works_many_times() {
|
||||||
struct UseEffectFunction {}
|
#[function_component(UseEffectComponent)]
|
||||||
impl FunctionProvider for UseEffectFunction {
|
fn use_effect_comp() -> Html {
|
||||||
type TProps = ();
|
let counter = use_state(|| 0);
|
||||||
|
let counter_clone = counter.clone();
|
||||||
|
|
||||||
fn run(_: &Self::TProps) -> HtmlResult {
|
use_effect_with_deps(
|
||||||
let counter = use_state(|| 0);
|
move |_| {
|
||||||
let counter_clone = counter.clone();
|
if *counter_clone < 4 {
|
||||||
|
counter_clone.set(*counter_clone + 1);
|
||||||
|
}
|
||||||
|
|| {}
|
||||||
|
},
|
||||||
|
*counter,
|
||||||
|
);
|
||||||
|
|
||||||
use_effect_with_deps(
|
html! {
|
||||||
move |_| {
|
<div>
|
||||||
if *counter_clone < 4 {
|
{ "The test result is" }
|
||||||
counter_clone.set(*counter_clone + 1);
|
<div id="result">{ *counter }</div>
|
||||||
}
|
{ "\n" }
|
||||||
|| {}
|
</div>
|
||||||
},
|
|
||||||
*counter,
|
|
||||||
);
|
|
||||||
|
|
||||||
Ok(html! {
|
|
||||||
<div>
|
|
||||||
{ "The test result is" }
|
|
||||||
<div id="result">{ *counter }</div>
|
|
||||||
{ "\n" }
|
|
||||||
</div>
|
|
||||||
})
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
type UseEffectComponent = FunctionComponent<UseEffectFunction>;
|
|
||||||
yew::start_app_in_element::<UseEffectComponent>(
|
yew::start_app_in_element::<UseEffectComponent>(
|
||||||
gloo_utils::document().get_element_by_id("output").unwrap(),
|
gloo_utils::document().get_element_by_id("output").unwrap(),
|
||||||
);
|
);
|
||||||
@ -121,32 +106,28 @@ fn use_effect_works_many_times() {
|
|||||||
|
|
||||||
#[wasm_bindgen_test]
|
#[wasm_bindgen_test]
|
||||||
fn use_effect_works_once() {
|
fn use_effect_works_once() {
|
||||||
struct UseEffectFunction {}
|
#[function_component(UseEffectComponent)]
|
||||||
impl FunctionProvider for UseEffectFunction {
|
fn use_effect_comp() -> Html {
|
||||||
type TProps = ();
|
let counter = use_state(|| 0);
|
||||||
|
let counter_clone = counter.clone();
|
||||||
|
|
||||||
fn run(_: &Self::TProps) -> HtmlResult {
|
use_effect_with_deps(
|
||||||
let counter = use_state(|| 0);
|
move |_| {
|
||||||
let counter_clone = counter.clone();
|
counter_clone.set(*counter_clone + 1);
|
||||||
|
|| panic!("Destructor should not have been called")
|
||||||
|
},
|
||||||
|
(),
|
||||||
|
);
|
||||||
|
|
||||||
use_effect_with_deps(
|
html! {
|
||||||
move |_| {
|
<div>
|
||||||
counter_clone.set(*counter_clone + 1);
|
{ "The test result is" }
|
||||||
|| panic!("Destructor should not have been called")
|
<div id="result">{ *counter }</div>
|
||||||
},
|
{ "\n" }
|
||||||
(),
|
</div>
|
||||||
);
|
|
||||||
|
|
||||||
Ok(html! {
|
|
||||||
<div>
|
|
||||||
{ "The test result is" }
|
|
||||||
<div id="result">{ *counter }</div>
|
|
||||||
{ "\n" }
|
|
||||||
</div>
|
|
||||||
})
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
type UseEffectComponent = FunctionComponent<UseEffectFunction>;
|
|
||||||
yew::start_app_in_element::<UseEffectComponent>(
|
yew::start_app_in_element::<UseEffectComponent>(
|
||||||
gloo_utils::document().get_element_by_id("output").unwrap(),
|
gloo_utils::document().get_element_by_id("output").unwrap(),
|
||||||
);
|
);
|
||||||
@ -156,45 +137,41 @@ fn use_effect_works_once() {
|
|||||||
|
|
||||||
#[wasm_bindgen_test]
|
#[wasm_bindgen_test]
|
||||||
fn use_effect_refires_on_dependency_change() {
|
fn use_effect_refires_on_dependency_change() {
|
||||||
struct UseEffectFunction {}
|
#[function_component(UseEffectComponent)]
|
||||||
impl FunctionProvider for UseEffectFunction {
|
fn use_effect_comp() -> Html {
|
||||||
type TProps = ();
|
let number_ref = use_mut_ref(|| 0);
|
||||||
|
let number_ref_c = number_ref.clone();
|
||||||
fn run(_: &Self::TProps) -> HtmlResult {
|
let number_ref2 = use_mut_ref(|| 0);
|
||||||
let number_ref = use_mut_ref(|| 0);
|
let number_ref2_c = number_ref2.clone();
|
||||||
let number_ref_c = number_ref.clone();
|
let arg = *number_ref.borrow_mut().deref_mut();
|
||||||
let number_ref2 = use_mut_ref(|| 0);
|
let counter = use_state(|| 0);
|
||||||
let number_ref2_c = number_ref2.clone();
|
use_effect_with_deps(
|
||||||
let arg = *number_ref.borrow_mut().deref_mut();
|
move |dep| {
|
||||||
let counter = use_state(|| 0);
|
let mut ref_mut = number_ref_c.borrow_mut();
|
||||||
use_effect_with_deps(
|
let inner_ref_mut = ref_mut.deref_mut();
|
||||||
move |dep| {
|
if *inner_ref_mut < 1 {
|
||||||
let mut ref_mut = number_ref_c.borrow_mut();
|
*inner_ref_mut += 1;
|
||||||
let inner_ref_mut = ref_mut.deref_mut();
|
assert_eq!(dep, &0);
|
||||||
if *inner_ref_mut < 1 {
|
} else {
|
||||||
*inner_ref_mut += 1;
|
assert_eq!(dep, &1);
|
||||||
assert_eq!(dep, &0);
|
}
|
||||||
} else {
|
counter.set(10); // we just need to make sure it does not panic
|
||||||
assert_eq!(dep, &1);
|
move || {
|
||||||
}
|
counter.set(11);
|
||||||
counter.set(10); // we just need to make sure it does not panic
|
*number_ref2_c.borrow_mut().deref_mut() += 1;
|
||||||
move || {
|
}
|
||||||
counter.set(11);
|
},
|
||||||
*number_ref2_c.borrow_mut().deref_mut() += 1;
|
arg,
|
||||||
}
|
);
|
||||||
},
|
html! {
|
||||||
arg,
|
<div>
|
||||||
);
|
{"The test result is"}
|
||||||
Ok(html! {
|
<div id="result">{*number_ref.borrow_mut().deref_mut()}{*number_ref2.borrow_mut().deref_mut()}</div>
|
||||||
<div>
|
{"\n"}
|
||||||
{"The test result is"}
|
</div>
|
||||||
<div id="result">{*number_ref.borrow_mut().deref_mut()}{*number_ref2.borrow_mut().deref_mut()}</div>
|
|
||||||
{"\n"}
|
|
||||||
</div>
|
|
||||||
})
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
type UseEffectComponent = FunctionComponent<UseEffectFunction>;
|
|
||||||
yew::start_app_in_element::<UseEffectComponent>(
|
yew::start_app_in_element::<UseEffectComponent>(
|
||||||
gloo_utils::document().get_element_by_id("output").unwrap(),
|
gloo_utils::document().get_element_by_id("output").unwrap(),
|
||||||
);
|
);
|
||||||
|
|||||||
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,30 +31,27 @@ impl Reducible for CounterState {
|
|||||||
|
|
||||||
#[wasm_bindgen_test]
|
#[wasm_bindgen_test]
|
||||||
fn use_reducer_works() {
|
fn use_reducer_works() {
|
||||||
struct UseReducerFunction {}
|
#[function_component(UseReducerComponent)]
|
||||||
impl FunctionProvider for UseReducerFunction {
|
fn use_reducer_comp() -> Html {
|
||||||
type TProps = ();
|
let counter = use_reducer(|| CounterState { counter: 10 });
|
||||||
fn run(_: &Self::TProps) -> HtmlResult {
|
|
||||||
let counter = use_reducer(|| CounterState { counter: 10 });
|
|
||||||
|
|
||||||
let counter_clone = counter.clone();
|
let counter_clone = counter.clone();
|
||||||
use_effect_with_deps(
|
use_effect_with_deps(
|
||||||
move |_| {
|
move |_| {
|
||||||
counter_clone.dispatch(1);
|
counter_clone.dispatch(1);
|
||||||
|| {}
|
|| {}
|
||||||
},
|
},
|
||||||
(),
|
(),
|
||||||
);
|
);
|
||||||
Ok(html! {
|
html! {
|
||||||
<div>
|
<div>
|
||||||
{"The test result is"}
|
{"The test result is"}
|
||||||
<div id="result">{counter.counter}</div>
|
<div id="result">{counter.counter}</div>
|
||||||
{"\n"}
|
{"\n"}
|
||||||
</div>
|
</div>
|
||||||
})
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
type UseReducerComponent = FunctionComponent<UseReducerFunction>;
|
|
||||||
yew::start_app_in_element::<UseReducerComponent>(
|
yew::start_app_in_element::<UseReducerComponent>(
|
||||||
gloo_utils::document().get_element_by_id("output").unwrap(),
|
gloo_utils::document().get_element_by_id("output").unwrap(),
|
||||||
);
|
);
|
||||||
@ -80,42 +77,39 @@ impl Reducible for ContentState {
|
|||||||
|
|
||||||
#[wasm_bindgen_test]
|
#[wasm_bindgen_test]
|
||||||
fn use_reducer_eq_works() {
|
fn use_reducer_eq_works() {
|
||||||
struct UseReducerFunction {}
|
#[function_component(UseReducerComponent)]
|
||||||
impl FunctionProvider for UseReducerFunction {
|
fn use_reducer_comp() -> Html {
|
||||||
type TProps = ();
|
let content = use_reducer_eq(|| ContentState {
|
||||||
fn run(_: &Self::TProps) -> HtmlResult {
|
content: HashSet::default(),
|
||||||
let content = use_reducer_eq(|| ContentState {
|
});
|
||||||
content: HashSet::default(),
|
|
||||||
});
|
|
||||||
|
|
||||||
let render_count = use_mut_ref(|| 0);
|
let render_count = use_mut_ref(|| 0);
|
||||||
|
|
||||||
let render_count = {
|
let render_count = {
|
||||||
let mut render_count = render_count.borrow_mut();
|
let mut render_count = render_count.borrow_mut();
|
||||||
*render_count += 1;
|
*render_count += 1;
|
||||||
|
|
||||||
*render_count
|
*render_count
|
||||||
};
|
};
|
||||||
|
|
||||||
let add_content_a = {
|
let add_content_a = {
|
||||||
let content = content.clone();
|
let content = content.clone();
|
||||||
Callback::from(move |_| content.dispatch("A".to_string()))
|
Callback::from(move |_| content.dispatch("A".to_string()))
|
||||||
};
|
};
|
||||||
|
|
||||||
let add_content_b = Callback::from(move |_| content.dispatch("B".to_string()));
|
let add_content_b = Callback::from(move |_| content.dispatch("B".to_string()));
|
||||||
|
|
||||||
Ok(html! {
|
html! {
|
||||||
<>
|
<>
|
||||||
<div>
|
<div>
|
||||||
{"This component has been rendered: "}<span id="result">{render_count}</span>{" Time(s)."}
|
{"This component has been rendered: "}<span id="result">{render_count}</span>{" Time(s)."}
|
||||||
</div>
|
</div>
|
||||||
<button onclick={add_content_a} id="add-a">{"Add A to Content"}</button>
|
<button onclick={add_content_a} id="add-a">{"Add A to Content"}</button>
|
||||||
<button onclick={add_content_b} id="add-b">{"Add B to Content"}</button>
|
<button onclick={add_content_b} id="add-b">{"Add B to Content"}</button>
|
||||||
</>
|
</>
|
||||||
})
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
type UseReducerComponent = FunctionComponent<UseReducerFunction>;
|
|
||||||
yew::start_app_in_element::<UseReducerComponent>(
|
yew::start_app_in_element::<UseReducerComponent>(
|
||||||
document().get_element_by_id("output").unwrap(),
|
document().get_element_by_id("output").unwrap(),
|
||||||
);
|
);
|
||||||
|
|||||||
@ -3,34 +3,29 @@ mod common;
|
|||||||
use common::obtain_result;
|
use common::obtain_result;
|
||||||
use std::ops::DerefMut;
|
use std::ops::DerefMut;
|
||||||
use wasm_bindgen_test::*;
|
use wasm_bindgen_test::*;
|
||||||
use yew::functional::{use_mut_ref, use_state, FunctionComponent, FunctionProvider};
|
use yew::prelude::*;
|
||||||
use yew::{html, HtmlResult};
|
|
||||||
|
|
||||||
wasm_bindgen_test::wasm_bindgen_test_configure!(run_in_browser);
|
wasm_bindgen_test::wasm_bindgen_test_configure!(run_in_browser);
|
||||||
|
|
||||||
#[wasm_bindgen_test]
|
#[wasm_bindgen_test]
|
||||||
fn use_ref_works() {
|
fn use_ref_works() {
|
||||||
struct UseRefFunction {}
|
#[function_component(UseRefComponent)]
|
||||||
impl FunctionProvider for UseRefFunction {
|
fn use_ref_comp() -> Html {
|
||||||
type TProps = ();
|
let ref_example = use_mut_ref(|| 0);
|
||||||
|
*ref_example.borrow_mut().deref_mut() += 1;
|
||||||
fn run(_: &Self::TProps) -> HtmlResult {
|
let counter = use_state(|| 0);
|
||||||
let ref_example = use_mut_ref(|| 0);
|
if *counter < 5 {
|
||||||
*ref_example.borrow_mut().deref_mut() += 1;
|
counter.set(*counter + 1)
|
||||||
let counter = use_state(|| 0);
|
}
|
||||||
if *counter < 5 {
|
html! {
|
||||||
counter.set(*counter + 1)
|
<div>
|
||||||
}
|
{"The test output is: "}
|
||||||
Ok(html! {
|
<div id="result">{*ref_example.borrow_mut().deref_mut() > 4}</div>
|
||||||
<div>
|
{"\n"}
|
||||||
{"The test output is: "}
|
</div>
|
||||||
<div id="result">{*ref_example.borrow_mut().deref_mut() > 4}</div>
|
|
||||||
{"\n"}
|
|
||||||
</div>
|
|
||||||
})
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
type UseRefComponent = FunctionComponent<UseRefFunction>;
|
|
||||||
yew::start_app_in_element::<UseRefComponent>(
|
yew::start_app_in_element::<UseRefComponent>(
|
||||||
gloo_utils::document().get_element_by_id("output").unwrap(),
|
gloo_utils::document().get_element_by_id("output").unwrap(),
|
||||||
);
|
);
|
||||||
|
|||||||
@ -2,34 +2,27 @@ mod common;
|
|||||||
|
|
||||||
use common::obtain_result;
|
use common::obtain_result;
|
||||||
use wasm_bindgen_test::*;
|
use wasm_bindgen_test::*;
|
||||||
use yew::functional::{
|
use yew::prelude::*;
|
||||||
use_effect_with_deps, use_state, use_state_eq, FunctionComponent, FunctionProvider,
|
|
||||||
};
|
|
||||||
use yew::{html, HtmlResult};
|
|
||||||
|
|
||||||
wasm_bindgen_test::wasm_bindgen_test_configure!(run_in_browser);
|
wasm_bindgen_test::wasm_bindgen_test_configure!(run_in_browser);
|
||||||
|
|
||||||
#[wasm_bindgen_test]
|
#[wasm_bindgen_test]
|
||||||
fn use_state_works() {
|
fn use_state_works() {
|
||||||
struct UseStateFunction {}
|
#[function_component(UseComponent)]
|
||||||
impl FunctionProvider for UseStateFunction {
|
fn use_state_comp() -> Html {
|
||||||
type TProps = ();
|
let counter = use_state(|| 0);
|
||||||
|
if *counter < 5 {
|
||||||
fn run(_: &Self::TProps) -> HtmlResult {
|
counter.set(*counter + 1)
|
||||||
let counter = use_state(|| 0);
|
}
|
||||||
if *counter < 5 {
|
html! {
|
||||||
counter.set(*counter + 1)
|
<div>
|
||||||
}
|
{"Test Output: "}
|
||||||
return Ok(html! {
|
<div id="result">{*counter}</div>
|
||||||
<div>
|
{"\n"}
|
||||||
{"Test Output: "}
|
</div>
|
||||||
<div id="result">{*counter}</div>
|
|
||||||
{"\n"}
|
|
||||||
</div>
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
type UseComponent = FunctionComponent<UseStateFunction>;
|
|
||||||
yew::start_app_in_element::<UseComponent>(
|
yew::start_app_in_element::<UseComponent>(
|
||||||
gloo_utils::document().get_element_by_id("output").unwrap(),
|
gloo_utils::document().get_element_by_id("output").unwrap(),
|
||||||
);
|
);
|
||||||
@ -39,42 +32,38 @@ fn use_state_works() {
|
|||||||
|
|
||||||
#[wasm_bindgen_test]
|
#[wasm_bindgen_test]
|
||||||
fn multiple_use_state_setters() {
|
fn multiple_use_state_setters() {
|
||||||
struct UseStateFunction {}
|
#[function_component(UseComponent)]
|
||||||
impl FunctionProvider for UseStateFunction {
|
fn use_state_comp() -> Html {
|
||||||
type TProps = ();
|
let counter = use_state(|| 0);
|
||||||
|
let counter_clone = counter.clone();
|
||||||
fn run(_: &Self::TProps) -> HtmlResult {
|
use_effect_with_deps(
|
||||||
let counter = use_state(|| 0);
|
move |_| {
|
||||||
let counter_clone = counter.clone();
|
// 1st location
|
||||||
use_effect_with_deps(
|
counter_clone.set(*counter_clone + 1);
|
||||||
move |_| {
|
|| {}
|
||||||
// 1st location
|
},
|
||||||
counter_clone.set(*counter_clone + 1);
|
(),
|
||||||
|| {}
|
);
|
||||||
},
|
let another_scope = {
|
||||||
(),
|
let counter = counter.clone();
|
||||||
);
|
move || {
|
||||||
let another_scope = {
|
if *counter < 11 {
|
||||||
let counter = counter.clone();
|
// 2nd location
|
||||||
move || {
|
counter.set(*counter + 10)
|
||||||
if *counter < 11 {
|
|
||||||
// 2nd location
|
|
||||||
counter.set(*counter + 10)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
};
|
}
|
||||||
another_scope();
|
};
|
||||||
Ok(html! {
|
another_scope();
|
||||||
<div>
|
html! {
|
||||||
{ "Test Output: " }
|
<div>
|
||||||
// expected output
|
{ "Test Output: " }
|
||||||
<div id="result">{ *counter }</div>
|
// expected output
|
||||||
{ "\n" }
|
<div id="result">{ *counter }</div>
|
||||||
</div>
|
{ "\n" }
|
||||||
})
|
</div>
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
type UseComponent = FunctionComponent<UseStateFunction>;
|
|
||||||
yew::start_app_in_element::<UseComponent>(
|
yew::start_app_in_element::<UseComponent>(
|
||||||
gloo_utils::document().get_element_by_id("output").unwrap(),
|
gloo_utils::document().get_element_by_id("output").unwrap(),
|
||||||
);
|
);
|
||||||
@ -87,26 +76,21 @@ fn use_state_eq_works() {
|
|||||||
use std::sync::atomic::{AtomicUsize, Ordering};
|
use std::sync::atomic::{AtomicUsize, Ordering};
|
||||||
static RENDER_COUNT: AtomicUsize = AtomicUsize::new(0);
|
static RENDER_COUNT: AtomicUsize = AtomicUsize::new(0);
|
||||||
|
|
||||||
struct UseStateFunction {}
|
#[function_component(UseComponent)]
|
||||||
|
fn use_state_comp() -> Html {
|
||||||
|
RENDER_COUNT.fetch_add(1, Ordering::Relaxed);
|
||||||
|
let counter = use_state_eq(|| 0);
|
||||||
|
counter.set(1);
|
||||||
|
|
||||||
impl FunctionProvider for UseStateFunction {
|
html! {
|
||||||
type TProps = ();
|
<div>
|
||||||
|
{"Test Output: "}
|
||||||
fn run(_: &Self::TProps) -> HtmlResult {
|
<div id="result">{*counter}</div>
|
||||||
RENDER_COUNT.fetch_add(1, Ordering::Relaxed);
|
{"\n"}
|
||||||
let counter = use_state_eq(|| 0);
|
</div>
|
||||||
counter.set(1);
|
|
||||||
|
|
||||||
Ok(html! {
|
|
||||||
<div>
|
|
||||||
{"Test Output: "}
|
|
||||||
<div id="result">{*counter}</div>
|
|
||||||
{"\n"}
|
|
||||||
</div>
|
|
||||||
})
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
type UseComponent = FunctionComponent<UseStateFunction>;
|
|
||||||
yew::start_app_in_element::<UseComponent>(
|
yew::start_app_in_element::<UseComponent>(
|
||||||
gloo_utils::document().get_element_by_id("output").unwrap(),
|
gloo_utils::document().get_element_by_id("output").unwrap(),
|
||||||
);
|
);
|
||||||
|
|||||||
@ -39,13 +39,16 @@ If we build another component which keeps track of the an event,
|
|||||||
instead of copying the code we can move the logic into a custom hook.
|
instead of copying the code we can move the logic into a custom hook.
|
||||||
|
|
||||||
We'll start by creating a new function called `use_event`.
|
We'll start by creating a new function called `use_event`.
|
||||||
The `use_` prefix conventionally denotes that a function is a hook.
|
The `use_` prefix denotes that a function is a hook.
|
||||||
This function will take an event target, a event type and a callback.
|
This function will take an event target, a event type and a callback.
|
||||||
|
All hooks must be marked by `#[hook]` to function as as hook.
|
||||||
```rust
|
```rust
|
||||||
use web_sys::{Event, EventTarget};
|
use web_sys::{Event, EventTarget};
|
||||||
use std::borrow::Cow;
|
use std::borrow::Cow;
|
||||||
use gloo::events::EventListener;
|
use gloo::events::EventListener;
|
||||||
|
use yew::prelude::*;
|
||||||
|
|
||||||
|
#[hook]
|
||||||
pub fn use_event<E, F>(target: &EventTarget, event_type: E, callback: F)
|
pub fn use_event<E, F>(target: &EventTarget, event_type: E, callback: F)
|
||||||
where
|
where
|
||||||
E: Into<Cow<'static, str>>,
|
E: Into<Cow<'static, str>>,
|
||||||
@ -65,6 +68,7 @@ use std::borrow::Cow;
|
|||||||
use std::rc::Rc;
|
use std::rc::Rc;
|
||||||
use gloo::events::EventListener;
|
use gloo::events::EventListener;
|
||||||
|
|
||||||
|
#[hook]
|
||||||
pub fn use_event<E, F>(target: &EventTarget, event_type: E, callback: F)
|
pub fn use_event<E, F>(target: &EventTarget, event_type: E, callback: F)
|
||||||
where
|
where
|
||||||
E: Into<Cow<'static, str>>,
|
E: Into<Cow<'static, str>>,
|
||||||
|
|||||||
@ -40,12 +40,18 @@ The `#[function_component]` attribute is a procedural macro which automatically
|
|||||||
Hooks are functions that let you "hook into" components' state and/or lifecycle and perform
|
Hooks are functions that let you "hook into" components' state and/or lifecycle and perform
|
||||||
actions. Yew comes with a few pre-defined Hooks. You can also create your own.
|
actions. Yew comes with a few pre-defined Hooks. You can also create your own.
|
||||||
|
|
||||||
|
Hooks can only be used at the following locations:
|
||||||
|
- Top level of a function / hook.
|
||||||
|
- If condition inside a function / hook, given it's not already branched.
|
||||||
|
- Match condition inside a function / hook, given it's not already branched.
|
||||||
|
- Blocks inside a function / hook, given it's not already branched.
|
||||||
|
|
||||||
#### Pre-defined Hooks
|
#### Pre-defined Hooks
|
||||||
|
|
||||||
Yew comes with the following predefined Hooks:
|
Yew comes with the following predefined Hooks:
|
||||||
- [`use_state`](./../function-components/pre-defined-hooks.mdx#use_state)
|
- [`use_state`](./../function-components/pre-defined-hooks.mdx#use_state)
|
||||||
- [`use_state_eq`](./../function-components/pre-defined-hooks.mdx#use_state_eq)
|
- [`use_state_eq`](./../function-components/pre-defined-hooks.mdx#use_state_eq)
|
||||||
- [`use_ref`](./../function-components/pre-defined-hooks.mdx#use_ref)
|
- [`use_memo`](./../function-components/pre-defined-hooks.mdx#use_memo)
|
||||||
- [`use_mut_ref`](./../function-components/pre-defined-hooks.mdx#use_mut_ref)
|
- [`use_mut_ref`](./../function-components/pre-defined-hooks.mdx#use_mut_ref)
|
||||||
- [`use_node_ref`](./../function-components/pre-defined-hooks.mdx#use_node_ref)
|
- [`use_node_ref`](./../function-components/pre-defined-hooks.mdx#use_node_ref)
|
||||||
- [`use_reducer`](./../function-components/pre-defined-hooks.mdx#use_reducer)
|
- [`use_reducer`](./../function-components/pre-defined-hooks.mdx#use_reducer)
|
||||||
|
|||||||
@ -64,22 +64,29 @@ re-render when the setter receives a value that `prev_state != next_state`.
|
|||||||
|
|
||||||
This hook requires the state object to implement `PartialEq`.
|
This hook requires the state object to implement `PartialEq`.
|
||||||
|
|
||||||
## `use_ref`
|
## `use_memo`
|
||||||
`use_ref` is used for obtaining an immutable reference to a value.
|
`use_memo` is used for obtaining an immutable reference to a memoized value.
|
||||||
Its state persists across renders.
|
Its state persists across renders.
|
||||||
|
Its value will be recalculated only if any of the dependencies values change.
|
||||||
|
|
||||||
`use_ref` can be useful for keeping things in scope for the lifetime of the component, so long as
|
`use_memo` can be useful for keeping things in scope for the lifetime of the component, so long as
|
||||||
you don't store a clone of the resulting `Rc` anywhere that outlives the component.
|
you don't store a clone of the resulting `Rc` anywhere that outlives the component.
|
||||||
|
|
||||||
If you need a mutable reference, consider using [`use_mut_ref`](#use_mut_ref).
|
|
||||||
If you need the component to be re-rendered on state change, consider using [`use_state`](#use_state).
|
|
||||||
|
|
||||||
```rust
|
```rust
|
||||||
use yew::{function_component, html, use_ref, use_state, Callback, Html};
|
use yew::{function_component, html, use_memo, use_state, Callback, Html, Properties};
|
||||||
|
|
||||||
#[function_component(UseRef)]
|
#[derive(PartialEq, Properties)]
|
||||||
fn ref_hook() -> Html {
|
pub struct Props {
|
||||||
let message = use_ref(|| "Some Expensive State.".to_string());
|
pub step: usize,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[function_component(UseMemo)]
|
||||||
|
fn ref_hook(props: &Props) -> Html {
|
||||||
|
// Will only get recalculated if `props.step` value changes
|
||||||
|
let message = use_memo(
|
||||||
|
|step| format!("{}. Do Some Expensive Calculation", step),
|
||||||
|
props.step
|
||||||
|
);
|
||||||
|
|
||||||
html! {
|
html! {
|
||||||
<div>
|
<div>
|
||||||
|
|||||||
@ -60,6 +60,7 @@ struct User {
|
|||||||
name: String,
|
name: String,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[hook]
|
||||||
fn use_user() -> SuspensionResult<User> {
|
fn use_user() -> SuspensionResult<User> {
|
||||||
match load_user() {
|
match load_user() {
|
||||||
// If a user is loaded, then we return it as Ok(user).
|
// If a user is loaded, then we return it as Ok(user).
|
||||||
@ -96,6 +97,7 @@ fn on_load_user_complete<F: FnOnce()>(_fn: F) {
|
|||||||
todo!() // implementation omitted.
|
todo!() // implementation omitted.
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[hook]
|
||||||
fn use_user() -> SuspensionResult<User> {
|
fn use_user() -> SuspensionResult<User> {
|
||||||
match load_user() {
|
match load_user() {
|
||||||
// If a user is loaded, then we return it as Ok(user).
|
// If a user is loaded, then we return it as Ok(user).
|
||||||
|
|||||||
@ -6,3 +6,12 @@ title: "From 0.19.0 to 0.20.0"
|
|||||||
|
|
||||||
This method of controlling body has caused issues in event registration and
|
This method of controlling body has caused issues in event registration and
|
||||||
SSR hydration. They have been removed. Read more in the [github issue](https://github.com/yewstack/yew/pull/2346).
|
SSR hydration. They have been removed. Read more in the [github issue](https://github.com/yewstack/yew/pull/2346).
|
||||||
|
|
||||||
|
## New Hooks and Function Components API
|
||||||
|
|
||||||
|
The Function Components and Hooks API are re-implemented with a different mechanism:
|
||||||
|
|
||||||
|
- User-defined hooks are now required to have a prefix `use_` and must be marked with the `#[hook]` attribute.
|
||||||
|
- Hooks will now report compile errors if they are not called from the top level of a function component
|
||||||
|
or a user defined hook. The limitation existed in the previous version of Yew as well. In this version,
|
||||||
|
It is reported as a compile time error.
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user