Yew: Proposal: Hooks API

Created on 14 Mar 2020  路  41Comments  路  Source: yewstack/yew

Is your feature request related to a problem? Please describe.
Currently component classes are required to have (stateful) components. They contain state management and lifecycle methods, requiring a message to be sent back for changes. This process can be simplified with functional components that use hooks.

Describe the solution you'd like
Introducing: hooks. Hooks are used in React to enable components, complex or simple, whose state is managed by the framework. This enables developers to progress faster and avoid some pitfalls. The hooks API is described at https://reactjs.org/docs/hooks-reference.html

Describe alternatives you've considered
Hooks aren't strictly necessary, neither for react nor yew, so they are optional. But they do have clear advantages, not least being easier to think about than messages.

Implementation
I propose (and have implemented) the following api:

use_state is the most basic hook. It outputs a state, provided by initial_state initially, and a function to update that state. Once the set_state method is called, the component rerenders with the call to use_state outputting that new state instead of the initial state.

pub fn use_state<T, F>(initial_state_fn: F) -> (Rc<T>, Box<impl Fn(T)>)
where
    F: FnOnce() -> T,
    T: 'static,
{
 // ...
}

use_effect lets you initialize a component and recompute if selected state changes. It is provided a closure and its arguments. If any of the arguments, which need to implement ==, change, the provided closure is executed again with those arguments and not executed again until the arguments change. There are use_effect1 through use_effect5 for number of arguments, while not giving any arguments executes the closure only once (it is debatable whether it should execute once or always in that case)

pub fn use_effect1<F, Destructor, T1>(callback: Box<F>, o1: T1)
where
    F: FnOnce(&T1) -> Destructor,
    Destructor: FnOnce() + 'static,
    T1: PartialEq + 'static,
{
  // ...
}

use_reducer2 is an advanced use_state function that lets you externalize computations of state change and initial state. It is given a function that combines an action and the previous state, as well as an initial state argument and a function that computes it into the initial state.
It returns the current state and a dispatch(Action) function to update the state.

pub fn use_reducer2<Action: 'static, Reducer, State: 'static, InitialState, InitFn>(
    reducer: Reducer,
    initial_state: InitialState,
    init: InitFn,
) -> (Rc<State>, Box<impl Fn(Action)>)
where
    Reducer: Fn(Rc<State>, Action) -> State + 'static,
    InitFn: Fn(InitialState) -> State,
{
  // ...
}

*use_reducer1 is a variaton that takes the initial state directly instead of a compute function to compute it.

pub fn use_reducer1<Action: 'static, Reducer, State: 'static>(
    reducer: Reducer,
    initial_state: State,
) -> (Rc<State>, Box<impl Fn(Action)>)
where
    Reducer: Fn(Rc<State>, Action) -> State + 'static,
{
  // ...
}

use_ref lets you have e RefCell of a state in your component that you can update yourself as need be

pub fn use_ref<T: 'static, InitialProvider>(initial_value: InitialProvider) -> Rc<RefCell<T>>
where
    InitialProvider: FnOnce() -> T,
{
  // ...
}

Reference implementation (needs to be cleaned up): https://pastebin.com/rWMn7YBX
Example uses follow.

proposal

Most helpful comment

@jkelleyrtp Seems like you were beaten to it: https://github.com/yewstack/yew/pull/1638

All 41 comments

Awesome!!

Basic component:
image
A simple counter:
image
use_effect to recompute a value as it changes:
image
Incrementing a counter using use_reducer:
image
Counting renders using use_ref:
image

Questions are: Should this be implemented into yew or be an external library and in case of the former where should it be placed :)
Oh also I still need to write the tests

How about we start by putting it into a new crate inside /crates in this repo? That's the plan for yew-router as well.

Good idea. I refactored and optimized the code and added comments throughout the use_hook function. I'll now prepare a pull request and do the tests :)

I noticed that #1032 and #1036 got merged and 0.14.3 was released later on, including these commits.

Is it correct that the yew_functional crate still needs to be published independently before use_state, use_reducer and use_effect can be used?

@Tehnix yes, it hasn't been released yet. But you can use a github link in your Cargo.toml file to start trying it out :)

@jstarry ah, didn't know that was possible in Cargo, thanks!

yew = { git = "https://github.com/yewstack/yew", rev = "666240c" }
yew-functional = { git = "https://github.com/yewstack/yew", rev = "666240c" }

seems to do the trick :)

EDIT: Updated for later yew and changes to Cargo on nightly.

Should we look into the composition API provided in vue 3? It is a bit similar to Hooks API in react.

Yes, I think we should. I think some of their API design choices are more easy to understand than React's. Thanks for the recommendation

I haven't used yew that much, but I think hooks would complicate it when it doesn't need to be. The component/trait based system that there is now is simple enough, if slightly verbose, and it's very clear how components get updated and rerendered. I think it would work well as a separate crate for those who want to write functional components, but I don't think that there's a problem with the component system as it exists now that would necessitate transitioning to functional components.

I'm not sure if it's possible with yew's API design, but my feature request for this is to also ship in hooks for component classes (as an extension maybe?), or at least have some supported way to use them.

One of my biggest annoyance with React's hooks is how basically nonexistent is support for class components (you could do it, but it's ugly and technically a hack). Once you start using hooks-based reducer, custom hooks, etc you're kinda locked in to having all-functional components even though sometimes you really want to use a class component for it.

It would be really nice if we could address this in yew's API design.

I think it would work well as a separate crate for those who want to write functional components, but I don't think that there's a problem with the component system as it exists now that would necessitate transitioning to functional components.

@coolreader18 We have indeed decided to build functional component support as a separate crate. The existing trait based system isn't going anywhere 馃槈

my feature request for this is to also ship in hooks for component classes (as an extension maybe?), or at least have some supported way to use them.

@atsuzaki interesting request, will definitely look into this, thanks for the suggestion!

@Tehnix yes, it hasn't been released yet. But you can use a github link in your Cargo.toml file to start trying it out :)

how to use yew-functional in Cargo.toml? yew-functional = { git = "https://github.com/yewstack/yew", path = "crates/functional", features = ["web_sys"] } don't work.

I think with using git dependencies with workspaces, you don't need to specify path, it just finds it in the workspace root. Try just leaving out the path = ... field. Also, yew-functional doesn't have a web-sys feature, you have to depend on plain yew for that.

I think with using git dependencies with workspaces, you don't need to specify path, it just finds it in the workspace root. Try just leaving out the path = ... field. Also, yew-functional doesn't have a web-sys feature, you have to depend on plain yew for that.

thank you!
I use

[dependencies]
log = "0.4"
yew = "0.16.2"
yew-router = { version="0.13.0", features = ["web_sys"] }
wasm-bindgen = "0.2.57"
wasm-logger = "0.2.0"
wee_alloc = "0.4.5"
yew-functional = { git = "https://github.com/yewstack/yew"}

but get a error: perhaps two different versions of crate yew are being used?

What error is it specifically? Maybe you could try making yew and yew-router git dependencies too?

What error is it specifically? Maybe you could try making yew and yew-router git dependencies too?

thank you! I succeed!

@anthhub I've updated my comment to reflect the changes to yew and Cargo since I posted the original comment :) Indeed it's removing path, optionally putting a rev on, and also doing that for yew (and any other yew crates) to make sure the types are aligned.

@anthhub I've updated my comment to reflect the changes to yew and Cargo since I posted the original comment :) Indeed it's removing path, optionally putting a rev on, and also doing that for yew (and any other yew crates) to make sure the types are aligned.

thank you! :)

use_effect how to use async function to featch data?

   use_effect_with_deps(
            async |_| {
                fun().await;

                return || ();
            },
            (),
        );

get a erorr: expected a std::ops::FnOnce<()> closure, found impl core::future::future::Future

use_effect how to use async function to featch data?

   use_effect_with_deps(
            async |_| {
                fun().await;

                return || ();
            },
            (),
        );

get a erorr: expected a std::ops::FnOnce<()> closure, found impl core::future::future::Future

i have got a solution to resolve it:

pub fn send_future<F, H>(future: F, handler: H)
where
    F: Future<Output = JsValue> + 'static,
    H: Fn(JsValue) -> ()  + 'static,
{
    spawn_local(async move {
       let rs = future.await;
       handler(rs);
    });
}

@jstarry Hey :)
Could you outline what we need to get the functional components going, other than reacting to ergonomics feedback etc?
One thing I certainly would like to do is a macro that converts functions to functional components.

@ZainlessBrombie An attribute macro for that would be awesome!

@ZainlessBrombie I've outlined a few next steps here: https://github.com/yewstack/yew/issues/1129! I think once we have more issues covering the remaining work we can dig in a bit more on what needs to be done. An issue for your proposed attribute macro would be nice as well!

Hey all, just made the macro. I didn't see it anywhere in the crate, so here's the source code if anyone wants to drop it in.

use proc_macro::{Span, TokenStream};
use quote::quote;
use syn::{FnArg, Ident, Item, Signature};

/// This macros autogenerates the functional component requirements for a yew function
/// This allows annotating individual functions as components
/// ```
/// #[derive(Default, Properties)]
/// pub struct CounterProps { count: u32 }
///
/// #[FunctionalComponent]
/// fn counter(props: &CounterProps) -> Html {
///     html!(<div>{props.count}</div>)
/// }
/// ```
#[proc_macro_attribute]
pub fn functional_component(_attr: TokenStream, item: TokenStream) -> TokenStream {
    if let Item::Fn(ref f) = &syn::parse(item).unwrap() {
        let Signature { ident, inputs, .. } = &f.sig;

        // Create an uppercased version of the function
        // We follow Rust lint patterns where structs are CamelCase
        // and functions are snake_case
        let mut name = ident.to_string();
        name.get_mut(0..1).as_mut().unwrap().make_ascii_uppercase();
        let new_name = Ident::new(name.as_str(), Span::call_site().into());

        // We come up with a generated intermediate version to implement the function provider trait against
        // This will be in the form of `__genCounter`
        // The expanded body might look like:
        // ```
        //      struct __genCounter;
        //      impl FunctionProvider for __genCounter {...};
        //      export type Counter = FunctionComponent<__genCounter>
        // ```
        let gen_name = Ident::new(
            ["__gen", name.as_str()].concat().as_str(),
            Span::call_site().into(),
        );

        // Get the token of the input properties
        let prop_token = get_prop_name(inputs.first().unwrap());

        // Get the body of the functional component
        let function_body = &f.block;

        let gen = quote! {
            pub struct #gen_name;
            impl FunctionProvider for #gen_name {
                type TProps = #prop_token;

                fn run(props: &Self::TProps) -> Html {
                    #function_body
                }
            }

            pub type #new_name = FunctionComponent<#gen_name>;
        };
        gen.into()
    } else {
        unimplemented!("FunctionalComponent macro only works on functions!");
    }
}

fn get_prop_name(args: &FnArg) -> &Ident {
    // Ensure the props is an function arg
    // We've captured an explicitly typed function argument set
    if let FnArg::Typed(r) = args {
        // Ensure the prop is a reference type
        // This means the prop is being pased in by reference
        if let syn::Type::Reference(a) = &r.ty.as_ref() {
            // Now we grab out [&PathCounter] token and extract the ident
            if let syn::Type::Path(pathitem) = a.elem.as_ref() {
                return &pathitem.path.segments.first().unwrap().ident;
            }
        }
    }

    unimplemented!("Functional component declaration is malformed");
}

Here's how I'm using it in my yew components:

use fpmacros::functional_component;
use yew::{html, Html, Properties};
use yew_functional::{use_state, FunctionComponent, FunctionProvider};

#[derive(Default, Clone, Debug, PartialEq, Properties)]
pub struct CounterProps {
    initial: i32,
}

#[functional_component]
pub fn counter(props: &CounterProps) -> Html {
    let (counter, set_counter) = use_state(|| 0);

    return html! {
        <div>
        { format!("Counter total: {}", counter) }
        <button onclick=CB(move |evt| set_counter(*counter + 1))>{"Click me"} </button>
        </div>
    };
}

I think this puts the boilerplate at react levels which makes me a very happy camper.

@jkelleyrtp this is beautiful 馃槩 I can't wait to use this!

To me, this was one of the main blockers for officially announcing the functional API / hooks. I'd really like to have this released inside yew-functional alongside the next yew release. An accompanying tutorial / blog post would be the cherry on top.

@jkelleyrtp this is beautiful 馃槩 I can't wait to use this!

To me, this was one of the main blockers for officially announcing the functional API / hooks. I'd really like to have this released inside yew-functional alongside the next yew release. An accompanying tutorial / blog post would be the cherry on top.

I can do both, just put a tutorial together for a small yew + rocket app which uses this.

The design space is quite huge, I wonder if we should just settle down with the first found method based on other existing stuff. I wish the API could be even simpler and more ergonomic.

let counter = use_state(0); // or we could also take in a function, we could do both at the same time
println!("{}", counter); // display
*counter += 1;

Rather than

let (counter, set_counter) = use_state(|| 0);
println!("{}", counter); // display
set_counter(*counter + 1);

That probably wouldn't be optimal; it isn't always clear when stuff like DerefMut triggers, which is why the impl shouldn't really have side effects. Though, you could probably have a type that implements Deref and has a set() method; that would probably be a very nice API.

Is there anything preventing yew-functional to be published on crates.io? I wrote a library using yew-functional but I can't publish it because crates.io does not allow me to use git dependencies.

I guess I should make a PR with the macro and tutorial :)

Is there any way to thread the prev: Rc<State> through the reducer without cloning, in the case where State does not implement Copy but merely Clone?

Is there any way to thread the prev: Rc<State> through the reducer without cloning, in the case where State does not implement Copy but merely Clone?

I don't understand. Can't you just clone the Rc?

The function you have to pass to use_reducer has signature (Rc<State>, Action) -> State. Rc::clone has type Rc<T> -> Rc<T>. Say you have an Action which in some case should just thread the previous state through. In general it's not possible to downcast an Rc<T> to a T because it may have multiple strong references. My question is that in this particular case, for the reducer function, it seems though it would be desirable to be able to thread the state through without having to call State::clone(&prev) and copying the state object in memory (This becomes seemingly necessary as soon as State is not Copy, say, if it contains a vector or a hashmap). I tried with try_unwrap in the obvious way but it seemed to crash the application, so perhaps there's some more nuanced way to do this that I'm not seeing, or some reason why this is not possible.

@NickHu That's actually a good point :thinking:
Maybe we should either return an Rc _or_ return an enum of ReducerResult::Change(State) / ReducerResult::Unchanged, where the latter does not cause a rerender. The former is more lenient while the latter encourages best pratice of not internally modifying the state.

Is there any conceptual reason why the old State should be kept around in memory anyway? Part of the benefits of rust is that with move semantics you can modify things in place safely where otherwise you would need immutability.

@jkelleyrtp Seems like you were beaten to it: https://github.com/yewstack/yew/pull/1638

@NickHu yes, the state needs to be accessed by the component invoking the hook, but the hook+reducer also needs to hold onto it, so we need to use an Rc. So there is no way we can have ownership of the state & edit in place.

@ZainlessBrombie My usecase (and I think this may be fairly common) is to have a State struct with a bunch of fields, where a lot of reducer actions only end up modifying a specific field. In this case I find myself using struct update syntax; something like this

match action {
  ...
  Action::Bar(v) => State {
    bar: getNextBar(v),
    ..State::clone(&prev)
  },
}

The problem is if I have other fields of State which are not Copy (say, a large hashmap), I don't want to be cloning that every time I handle this action. It seems to me that the way the current implementation is designed I would need to wrap these fields in an Rc also.

Hm. I see what you mean, but State is meant to be immutable, mutable state is what use_ref is for :thinking:
Maybe we need a hook that just provides a trigger for a rerender?
Also a Cow could be useful there.

Was this page helpful?
0 / 5 - 0 ratings

Related issues

IngwiePhoenix picture IngwiePhoenix  路  4Comments

sackery picture sackery  路  3Comments

Boscop picture Boscop  路  5Comments

wldcordeiro picture wldcordeiro  路  4Comments

kellytk picture kellytk  路  4Comments