-
-
Notifications
You must be signed in to change notification settings - Fork 1.4k
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
Comments
Awesome!! |
Questions are: Should this be implemented into yew or be an external library and in case of the former where should it be placed :) |
How about we start by putting it into a new crate inside |
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 yes, it hasn't been released yet. But you can use a github link in your |
@jstarry ah, didn't know that was possible in Cargo, thanks!
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. |
@coolreader18 We have indeed decided to build functional component support as a separate crate. The existing trait based system isn't going anywhere 😉
@atsuzaki interesting request, will definitely look into this, thanks for the suggestion! |
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 |
thank you! [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 |
What error is it specifically? Maybe you could try making |
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 |
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 |
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 |
Is there anything preventing |
I guess I should make a PR with the macro and tutorial :) |
Is there any way to thread the |
I don't understand. Can't you just clone the |
The function you have to pass to |
@NickHu That's actually a good point 🤔 |
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: #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 |
Hm. I see what you mean, but State is meant to be immutable, mutable state is what use_ref is for 🤔 |
For the #[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 |
This comment has been minimized.
This comment has been minimized.
Finally I manage to resolve it! It's my fault to make things more complex. I should simply use 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)}
</>
}
} |
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>
}
} |
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 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>
}
} |
What would the component render in case of |
In React, an error would propagate to an I think any component that takes a
I am just copying what React is doing which is to IMHO, currently one of the unergonomic parts in Yew is it forces any 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
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. |
Unfortunately,
That doesn't really matter. Yew is still v0.x.x and it's fine to break API where needed. |
I know this is still open, but is there a crate that can be installed? |
Yes, you can install |
Now that |
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.
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)
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.
*use_reducer1 is a variaton that takes the initial state directly instead of a compute function to compute it.
use_ref lets you have e RefCell of a state in your component that you can update yourself as need be
Reference implementation (needs to be cleaned up): https://pastebin.com/rWMn7YBX
Example uses follow.
The text was updated successfully, but these errors were encountered: