Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Proposal: Hooks API #1026

Closed
ZainlessBrombie opened this issue Mar 14, 2020 · 52 comments
Closed

Proposal: Hooks API #1026

ZainlessBrombie opened this issue Mar 14, 2020 · 52 comments
Labels

Comments

@ZainlessBrombie
Copy link
Collaborator

ZainlessBrombie commented Mar 14, 2020

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.

@ZainlessBrombie ZainlessBrombie added the feature-request A feature request label Mar 14, 2020
@jstarry
Copy link
Member

jstarry commented Mar 14, 2020

Awesome!!

@ZainlessBrombie
Copy link
Collaborator Author

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

@ZainlessBrombie
Copy link
Collaborator Author

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

@jstarry
Copy link
Member

jstarry commented Mar 14, 2020

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.

@ZainlessBrombie
Copy link
Collaborator Author

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 :)

@Tehnix
Copy link

Tehnix commented Apr 14, 2020

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?

@jstarry
Copy link
Member

jstarry commented Apr 14, 2020

@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 :)

@Tehnix
Copy link

Tehnix commented Apr 14, 2020

@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.

@jstarry jstarry removed the feature-request A feature request label Apr 25, 2020
@jstarry jstarry changed the title Introducing: The hooks API Proposal: Hooks API Apr 25, 2020
@pickfire
Copy link

pickfire commented May 4, 2020

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

@jstarry
Copy link
Member

jstarry commented May 4, 2020

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

@mkawalec mkawalec mentioned this issue May 4, 2020
@coolreader18
Copy link

coolreader18 commented May 10, 2020

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.

@atsuzaki
Copy link
Contributor

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.

@jstarry
Copy link
Member

jstarry commented May 10, 2020

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!

@anthhub
Copy link

anthhub commented Jun 21, 2020

@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.

@coolreader18
Copy link

coolreader18 commented Jun 21, 2020

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.

@anthhub
Copy link

anthhub commented Jun 21, 2020

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?

@coolreader18
Copy link

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

@anthhub
Copy link

anthhub commented Jun 22, 2020

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

thank you! I succeed!

@Tehnix
Copy link

Tehnix commented Jun 22, 2020

@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
Copy link

anthhub commented Jun 22, 2020

@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! :)

@anthhub
Copy link

anthhub commented Jun 27, 2020

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

@jkelleyrtp
Copy link
Contributor

@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.

@pickfire
Copy link

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);

@coolreader18
Copy link

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.

@lukechu10
Copy link
Contributor

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.

@jkelleyrtp
Copy link
Contributor

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

@NickHu
Copy link

NickHu commented Oct 31, 2020

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?

@lukechu10
Copy link
Contributor

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?

@NickHu
Copy link

NickHu commented Nov 1, 2020

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.

@ZainlessBrombie
Copy link
Collaborator Author

@NickHu That's actually a good point 🤔
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.

@NickHu
Copy link

NickHu commented Nov 3, 2020

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.

@NickHu
Copy link

NickHu commented Nov 4, 2020

@jkelleyrtp Seems like you were beaten to it: #1638

@ZainlessBrombie
Copy link
Collaborator Author

@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.

@NickHu
Copy link

NickHu commented Nov 4, 2020

@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.

@ZainlessBrombie
Copy link
Collaborator Author

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

@lukechu10
Copy link
Contributor

lukechu10 commented Feb 10, 2021

For the use_state hook, wouldn't it be more ergonomic if it only returned a struct rather than a tuple? It could be something like this:

#[derive(Clone)]
pub struct StateHandle<T> {
    value: Rc<T>,
}

impl<T> StateHandle<T> {
    /// Update the state and trigger a rerender.
    pub fn set(value: T) { }
}

impl<T> Deref for StateHandle<T> {
    type Target = T;

    fn deref(&self) -> &Self::Target { }
}

So we could use it like this:

let counter = use_state(|| 0);

html! {
    <button onclick=Callback::from(|_| counter.set(**counter + 1)) >{ "+1" }</button>
    <p>{ *counter }</p>
}

This way, when cloning the state into a closure (for callbacks), we only need to clone one Rc instead of two which should improve performance and ergonomics.

@yuchanns

This comment has been minimized.

@yuchanns
Copy link

With a functional component, how can I use the props in a closure?

#[derive(Default, Clone, Debug, PartialEq, Properties)]
pub struct Props {
	pub onclick: Callback<usize>,
	pub value: &'static str,
}

#[function_component(ButtonGroup)]
pub fn button_group(props: &Props) -> Html {
	let render_button = |i: usize| {
        html!{
          <button onclick={ Callback::from(move |_| &props.onclick.emit(i)) }>
            {&props.value}
          </button>
        }
    };
	html!{
	  <>
		{render_button(1)}
		{render_button(2)}
	  </>
	}
}

Then I got:

error[E0271]: type mismatch resolving `<[closure@src/button_group.rs:16:44: 16:77] as FnOnce<(_,)>>::Output == ()`
  --> src/button_group.rs:16:29
   |
16 |           <button onclick={ Callback::from(move |_| {&props.onclick.emit(i)}) }>
   |                             ^^^^^^^^^^^^^^ expected `&()`, found `()`
   |
   = note: required because of the requirements on the impl of `From<[closure@src/button_group.rs:16:44: 16:77]>` for `yew::Callback<_>`
   = note: required by `from`

Here is my Cargo.toml:

[dependencies]
wasm-bindgen = "0.2.67"
yew-functional = { git = "https://github.com/yewstack/yew"}
yew = { git = "https://github.com/yewstack/yew"}
yew-router = { git = "https://github.com/yewstack/yew"}

Finally I manage to resolve it! It's my fault to make things more complex. I should simply use Callback's clone method and everything goes well.

use std::rc::Rc;
use yew::prelude::*;
use yew_functional::*;

#[derive(Clone, PartialEq, Properties)]
struct Props {
    onclick: Callback<usize>,
}

#[function_component(ButtonOuter)]
pub fn button_outer() -> Html {
    let (number, set_number) = use_state(|| 0usize);
    let onclick = Callback::from(move |i: usize| set_number(i));
    html! {
        <div>
            <p>{number}</p>
            <ButtonGroup onclick=onclick />
        </div>
    }
}

#[function_component(ButtonGroup)]
fn button_group(props: &Props) -> Html {
    let render_button = |i: usize| {
        let onclick = props.onclick.clone();
        html! {
          <button onclick={ Callback::from(move |_| onclick.emit(i)) }>
            {i}
          </button>
        }
    };
    html! {
      <>
        {render_button(1)}
        {render_button(2)}
      </>
    }
}

@futursolo
Copy link
Member

futursolo commented Jun 12, 2021

Just my two cents, wouldn't it be better to have the function component to (optionally) be an async function and (optionally) to return a result?

type FcResult = anyhow::Result<Html>;

#[function_component(AsyncFc)]
async fn async_fc() -> FcResult {
    let resp: Rc<Response> = use_query().await?;
    Ok(html!{ <div>{resp.content}</div> })
}

Then we will be able to do something like React concurrent mode:

type Try = ErrorBoundary<FcResult>;
type Suspense = Suspense<FcResult>;

// Send the error back to the server, or do something else fancy here,
// like redirect to login on HTTP 401
fn report_error(e: anyhow::Error) {}


#[function_component(MyApp)]
fn my_app() -> Html {
    let fallback = html!{ <div>Loading...</div> };
    let error_fallback = html!{ <div>Sorry, something went wrong.</div> };

   html!{
        <Try on_catch=report_error fallback=error_fallback>
            <Suspense fallback=fallback>  // consumes Future<Output = FcResult> with spawn_local and returns FcResult.
                <AsyncFc />
            </Suspense>
        </Try>
    }
}

@futursolo
Copy link
Member

futursolo commented Jun 12, 2021

I just realised that the above version may never be able to render children instantly as they always have to be awaited first, which could cause the Loading... fallback being shown for a short time every time the content re-renders.

Updated example:

#[derive(thiserror::Error, Debug)]
enum FcError<E: std::error::Error + 'static> {
    #[error("Pending!")]
    Pending(Box<Future<Output = ()>>)
    #[error("Something went wrong.")]
    Error(#[from] E)
}

type AsyncFcResult = std::result::Result<Html, FcError<anyhow::Error>>;

// Not used in this example, but can be used to pass through errors
// for components that are between a `<Suspense />` and a `<Try />`.
// Allows multiple `<Suspense />` to share one `<Try />`.
type FcResult = anyhow::Result<Html>; 

#[function_component(AsyncFc)]
fn async_fc() -> AsyncFcResult {
    let resp: Rc<Response> = use_query()?;
    Ok(html!{ <div>{resp.content}</div> })
}

type Try = ErrorBoundary<anyhow::Error>;
type Suspense = Suspense<anyhow::Error>;

// Send the error back to the server, or do something else fancy here,
// like redirect to login on HTTP 401
fn report_error(e: anyhow::Error) {}

#[function_component(MyApp)]
fn my_app() -> Html {
    let fallback = html!{ <div>Loading...</div> };
    let error_fallback = html!{ <div>Sorry, something went wrong.</div> };

   html!{
        <Try on_catch=report_error fallback=error_fallback>
            <Suspense fallback=fallback>
                <AsyncFc />
            </Suspense>
        </Try>
    }
}

@ranile
Copy link
Member

ranile commented Jun 13, 2021

What would the component render in case of Err(_)? I believe these kind of components could be implemented without returning a Result<_, _>. Also, this needs to be implemented for both, function components and struct components to avoid feature discrepancy.

@futursolo
Copy link
Member

What would the component render in case of Err(_)?

In React, an error would propagate to an ErrorBoundary. Then that ErrorBoundary is responsible to render a fallback UI and handle that error as needed.

I think any component that takes a FcResult as "children" and returns an Html would suffice.
The common behaviour of an error boundary is to show a fallback UI and (optionally) send this error to backend for debug purpose. Hence I included a <Try /> to cater that behaviour.

I believe these kind of components could be implemented without returning a Result<_, _>.

I am just copying what React is doing which is to throw a Promise when the data is not ready.
If this can be done without modifying the function signature (hence compatible with existing class component), it's even better.

IMHO, currently one of the unergonomic parts in Yew is it forces any Err to be handled in the component via messages which requires a lot of boilerplate.

This also used to be true for React, because data fetching do not happen in the render process so errors needs to be handled manually.

But Suspense is different as they fetch data in the render process. They allow errors to to be "thrown" like a traditional error during the rendering process. So having a match children {Ok(m) => ... Err(e) => ...} helps the error handling to become more ergonomic.

Also, this needs to be implemented for both, function components and struct components to avoid feature discrepancy.

I thought about this, but I think this would break the API for struct components. It's fine in JavaScript as an Error can always be thrown without changing the function signature.

Although I mentioned a lot "React" in this comment, I am not expecting Yew to become React. I just think this feature will bring a very ergonomic way for data-fetching and code-splitting (when that comes to wasm) to Yew.

@ranile
Copy link
Member

ranile commented Jun 14, 2021

Unfortunately, Boxing a future and sending it down adds runtime overhead and there's no way to return an impl Future from a trait function (which is what, both function and struct components use). wasm_bindgen_futures isn't at a point where we can run message handling in a Promise. See this discussion on discord about async Component::update. There's more limitations of WASM than of Yew at this point when it comes to ergonomic data fetching.
I imagine we could have a wrapper that abstracts some of Yew's boilerplate but I doubt we could do much more than that right now.

I thought about this, but I think this would break the API for struct components.

That doesn't really matter. Yew is still v0.x.x and it's fine to break API where needed.

@11reed
Copy link

11reed commented Jun 25, 2021

I know this is still open, but is there a crate that can be installed?

@ranile
Copy link
Member

ranile commented Jun 25, 2021

I know this is still open, but is there a crate that can be installed?

Yes, you can install yew-functional via git. #1842 will merge that into yew

@ranile
Copy link
Member

ranile commented Aug 12, 2021

Now that yew-functional is merged into yew and will be released with the next version, this issue can be closed now

@siku2 siku2 closed this as completed Aug 12, 2021
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
Projects
None yet
Development

No branches or pull requests