This is unreleased documentation for Yew Next version.
For up-to-date documentation, see the latest version on docs.rs.

yew_macro/hook/
mod.rs

1use proc_macro2::{Span, TokenStream};
2use proc_macro_error::emit_error;
3use quote::quote;
4use syn::parse::{Parse, ParseStream};
5use syn::spanned::Spanned;
6use syn::{
7    visit_mut, AttrStyle, Attribute, Block, Expr, ExprPath, File, Ident, Item, ItemFn, LitStr,
8    Meta, MetaNameValue, ReturnType, Signature, Stmt, Token, Type,
9};
10
11mod body;
12mod lifetime;
13mod signature;
14
15pub use body::BodyRewriter;
16use signature::HookSignature;
17
18#[derive(Clone)]
19pub struct HookFn {
20    inner: ItemFn,
21}
22
23impl Parse for HookFn {
24    fn parse(input: ParseStream) -> syn::Result<Self> {
25        let func: ItemFn = input.parse()?;
26
27        let sig = func.sig.clone();
28
29        if sig.asyncness.is_some() {
30            emit_error!(sig.asyncness, "async functions can't be hooks");
31        }
32
33        if sig.constness.is_some() {
34            emit_error!(sig.constness, "const functions can't be hooks");
35        }
36
37        if sig.abi.is_some() {
38            emit_error!(sig.abi, "extern functions can't be hooks");
39        }
40
41        if sig.unsafety.is_some() {
42            emit_error!(sig.unsafety, "unsafe functions can't be hooks");
43        }
44
45        if !sig.ident.to_string().starts_with("use_") {
46            emit_error!(sig.ident, "hooks must have a name starting with `use_`");
47        }
48
49        Ok(Self { inner: func })
50    }
51}
52
53impl HookFn {
54    fn doc_attr(&self) -> Attribute {
55        let span = self.inner.span();
56
57        let sig_formatted = prettyplease::unparse(&File {
58            shebang: None,
59            attrs: vec![],
60            items: vec![Item::Fn(ItemFn {
61                block: Box::new(Block {
62                    brace_token: Default::default(),
63                    stmts: vec![Stmt::Expr(
64                        Expr::Path(ExprPath {
65                            attrs: vec![],
66                            qself: None,
67                            path: Ident::new("__yew_macro_dummy_function_body__", span).into(),
68                        }),
69                        None,
70                    )],
71                }),
72                ..self.inner.clone()
73            })],
74        });
75
76        let literal = LitStr::new(
77            &format!(
78                r#"
79# Note
80
81When used in function components and hooks, this hook is equivalent to:
82
83```
84{}
85```
86"#,
87                sig_formatted.replace(
88                    "__yew_macro_dummy_function_body__",
89                    "/* implementation omitted */"
90                )
91            ),
92            span,
93        );
94
95        Attribute {
96            pound_token: Default::default(),
97            style: AttrStyle::Outer,
98            bracket_token: Default::default(),
99            meta: Meta::NameValue(MetaNameValue {
100                path: Ident::new("doc", span).into(),
101                eq_token: Token![=](span),
102                value: Expr::Lit(syn::ExprLit {
103                    attrs: vec![],
104                    lit: literal.into(),
105                }),
106            }),
107        }
108    }
109}
110
111pub fn hook_impl(hook: HookFn) -> syn::Result<TokenStream> {
112    let doc_attr = hook.doc_attr();
113
114    let HookFn { inner: original_fn } = hook;
115
116    let ItemFn {
117        ref vis,
118        ref sig,
119        ref block,
120        ref attrs,
121    } = original_fn;
122    let mut block = *block.clone();
123
124    let hook_sig = HookSignature::rewrite(sig);
125
126    let Signature {
127        ref fn_token,
128        ref ident,
129        ref inputs,
130        output: ref hook_return_type,
131        ref generics,
132        ..
133    } = hook_sig.sig;
134
135    let output_type = &hook_sig.output_type;
136
137    let (impl_generics, ty_generics, where_clause) = generics.split_for_impl();
138    let call_generics = hook_sig.call_generics();
139
140    // We use _ctx so that if a hook does not use other hooks, it will not trigger unused_vars.
141    let ctx_ident = Ident::new("_ctx", Span::mixed_site());
142
143    let mut body_rewriter = BodyRewriter::new(ctx_ident.clone());
144    visit_mut::visit_block_mut(&mut body_rewriter, &mut block);
145
146    let inner_fn_ident = Ident::new("inner_fn", Span::mixed_site());
147    let input_args = hook_sig.input_args();
148
149    // there might be some overridden lifetimes in the return type.
150    let inner_fn_rt = match &sig.output {
151        ReturnType::Default => None,
152        ReturnType::Type(rarrow, _) => Some(quote! { #rarrow #output_type }),
153    };
154
155    let inner_fn = quote! { fn #inner_fn_ident #generics (#ctx_ident: &mut ::yew::functional::HookContext, #inputs) #inner_fn_rt #where_clause #block };
156
157    let inner_type_impl = if hook_sig.needs_boxing {
158        let with_output = !matches!(hook_sig.output_type, Type::ImplTrait(_),);
159        let inner_fn_rt = with_output.then_some(&inner_fn_rt);
160        let output_type = with_output.then_some(&output_type);
161
162        let hook_lifetime = &hook_sig.hook_lifetime;
163        let hook_lifetime_plus = quote! { #hook_lifetime + };
164
165        let boxed_inner_ident = Ident::new("boxed_inner", Span::mixed_site());
166        let boxed_fn_type = quote! { ::std::boxed::Box<dyn #hook_lifetime_plus ::std::ops::FnOnce(&mut ::yew::functional::HookContext) #inner_fn_rt> };
167
168        let as_boxed_fn = with_output.then(|| quote! { as #boxed_fn_type });
169
170        let generic_types = generics.type_params().map(|t| &t.ident);
171
172        // We need boxing implementation for `impl Trait` arguments.
173        quote! {
174            let #boxed_inner_ident = ::std::boxed::Box::new(
175                    move |#ctx_ident: &mut ::yew::functional::HookContext| #inner_fn_rt {
176                        #inner_fn_ident :: <#(#generic_types,)*> (#ctx_ident, #(#input_args,)*)
177                    }
178                ) #as_boxed_fn;
179
180            ::yew::functional::BoxedHook::<#hook_lifetime, #output_type>::new(#boxed_inner_ident)
181        }
182    } else {
183        let input_types = hook_sig.input_types();
184
185        let args_ident = Ident::new("args", Span::mixed_site());
186        let hook_struct_name = Ident::new("HookProvider", Span::mixed_site());
187
188        let phantom_types = hook_sig.phantom_types();
189        let phantom_lifetimes = hook_sig.phantom_lifetimes();
190
191        quote! {
192            struct #hook_struct_name #generics #where_clause {
193                _marker: ::std::marker::PhantomData<( #(#phantom_types,)* #(#phantom_lifetimes,)* )>,
194                #args_ident: (#(#input_types,)*),
195            }
196
197            #[automatically_derived]
198            impl #impl_generics ::yew::functional::Hook for #hook_struct_name #ty_generics #where_clause {
199                type Output = #output_type;
200
201                fn run(mut self, #ctx_ident: &mut ::yew::functional::HookContext) -> Self::Output {
202                    let (#(#input_args,)*) = self.#args_ident;
203
204                    #inner_fn_ident #call_generics (#ctx_ident, #(#input_args,)*)
205                }
206            }
207
208            #[automatically_derived]
209            impl #impl_generics #hook_struct_name #ty_generics #where_clause {
210                fn new(#inputs) -> Self {
211                   #hook_struct_name {
212                        _marker: ::std::marker::PhantomData,
213                        #args_ident: (#(#input_args,)*),
214                    }
215                }
216            }
217
218            #hook_struct_name #call_generics ::new(#(#input_args,)*)
219        }
220    };
221
222    // There're some weird issues with doc tests that it cannot detect return types properly.
223    // So we print original implementation instead.
224    let output = quote! {
225        #[cfg(not(doctest))]
226        #(#attrs)*
227        #doc_attr
228        #vis #fn_token #ident #generics (#inputs) #hook_return_type #where_clause {
229            #inner_fn
230
231            #inner_type_impl
232        }
233
234        #[cfg(doctest)]
235        #original_fn
236    };
237
238    Ok(output)
239}