Skip to content

Commit

Permalink
Upgraded Hook API (2) (#1780)
Browse files Browse the repository at this point in the history
* Feat: make hooks more ergonomic and easier to understand

* clippy, apply changes from review on PR (yew#1645)

* update docs

* fix docs and comment

* more ergonomic use_state

* update docs, fmt, clippy

* rename structs

* improve use_reducer API

* update use_reducer docs

* formatting

* Apply suggestions from code review

Co-authored-by: Jonathan Kelley <jkelleyrtp@gmail.com>
Co-authored-by: Simon <simon@siku2.io>
  • Loading branch information
3 people authored May 5, 2021
1 parent c91d3dc commit aa828d7
Show file tree
Hide file tree
Showing 15 changed files with 482 additions and 474 deletions.
79 changes: 12 additions & 67 deletions docs/concepts/function-components/custom-hooks.md
Original file line number Diff line number Diff line change
Expand Up @@ -39,75 +39,13 @@ The `use_` prefix conventionally denotes that a function is a hook.
This function will take no arguments and return `Rc<RefCell<Vec<String>>>`.
```rust
fn use_subscribe() -> Rc<RefCell<Vec<String>>> {
// ...
todo!()
}
```

The hook's logic goes inside the `use_hook`'s callback.
`use_hook` is the handler function for custom Hooks. It takes in 2 arguments: `hook_runner` and `initial_state_producer`.

`hook_runner` is where all the hook's logic goes. `use_hook` returns the value returned by this callback.
`hook_runner` itself takes 2 arguments: a mutable reference to the internal state of the hook and `hook_callback`.
`hook_callback` also takes in 2 arguments: a callback and, a bool indicating whether it is run post render of the component.
The callback takes in `internal_state` which is a mutable reference to the instance of the internal state and performs the actual mutations.
It returns `ShouldRender` bool.
`use_hook`'s second argument of `initial_state_producer` takes in a callback for creating an instance of the internal state.
The internal state is a struct which implements the `Hook` trait.

Now let's create the state struct for our `use_subscribe` hook.
```rust
/// `use_subscribe` internal state
struct UseSubscribeState {
/// holds all the messages received
pub messages: Rc<RefCell<Vec<String>>>,
}

impl Hook for UseSubscribeState {}
```

Now we'll modify `use_subscribe` to contain the actual logic.
```rust
fn use_subscribe() -> Rc<RefCell<Vec<String>>> {
use_hook(
// hook's handler. all the logic goes in here
|state: &mut UseSubscribeState, hook_callback| {
// calling other Hooks inside a hook
use_effect(move || {
let producer = EventBus::bridge(Callback::from(move |msg| {
hook_callback(
// where the mutations of state are performed
|state| {
(*state.messages).borrow_mut().deref_mut().push(msg);
true // should re-render
}, false // run post-render
)
}));

|| drop(producer)
});

// return from hook
state.messages.clone()
},
// initial state producer
|| UseSubscribeState { messages: Rc::new(RefCell::new(vec![])) },
)
}
```

We can now use our custom hook like this:
```rust
#[function_component(ShowMessages)]
pub fn show_messages() -> Html {
let state = use_subscribe();
let output = state.borrow().deref().into_iter().map(|it| html! { <p>{ it }</p> });

html! { <div>{ for output }</div> }
}
```

It's important to note that `use_hook` isn't necessarily required to create custom hooks
as they can just consist of other hooks. `use_hook` should generally be avoided.
This is a simple hook which can be created by combining other hooks. For this example, we'll two pre-defined hooks.
We'll use `use_state` hook to store the `Vec` for messages, so they persist between component re-renders.
We'll also use `use_effect` to subscribe to the `EventBus` `Agent` so the subscription can be tied to component's lifecycle.

```rust
fn use_subscribe() -> Rc<Vec<String>> {
Expand All @@ -124,4 +62,11 @@ fn use_subscribe() -> Rc<Vec<String>> {

state
}
```
```

Although this approach works in almost all cases, it can't be used to write primitive hooks like the pre-defined hooks we've been using already

### Writing primitive hooks

`use_hook` function is used to write such hooks. View the docs on [docs.rs]() for the documentation
and `hooks` directory to see implementations of pre-defined hooks.
59 changes: 17 additions & 42 deletions docs/concepts/function-components/pre-defined-hooks.md
Original file line number Diff line number Diff line change
Expand Up @@ -3,56 +3,33 @@ title: Pre-defined Hooks
description: The pre-defined Hooks that Yew comes with
---

:::note Why do Hooks return `Rc`?

In most cases, you'll be cloning the values returned from the Hooks.
As it is generally expensive to clone such values, they're `Rc`ed, so they can be cloned relatively cheaply.

The following example shows one of the most common cases which requires cloning the values:

```rust
let (text, set_text) = use_state(|| "Hello".to_owned());
let onclick = {
let text = Rc::clone(&text);
// Values must be moved into this closure so in order to use them later on, they must be cloned
Callback::from(move |_| set_text(format!("{} World", text)))
};

// If `text` wasn't cloned above, it would've been impossible to use it here
html! { text }
```
:::

## `use_state`

`use_state` is used to mange state in a function component.
It returns a `Rc` pointing to the value of the hook's state, and a setter function.
`use_state` is used to manage state in a function component.
It returns a `UseState` object which `Deref`s to the current value
and provides a `set` method to update the value.

The hook takes a function as input which determines the initial state.
This value remains up-to-date on subsequent renders.

The setter function is used to update the value and trigger a re-render.

### Example

```rust
#[function_component(UseState)]
fn state() -> Html {
let (
counter, // the returned state
set_counter // setter to update the state
) = use_state(|| 0);
let counter = use_state(|| 0);
let onclick = {
let counter = Rc::clone(&counter);
Callback::from(move |_| set_counter(*counter + 1))
let counter = counter.clone();
Callback::from(move |_| counter.set(*counter + 1))
};


html! {
<div>
<button onclick=onclick>{ "Increment value" }</button>
<p>
<b>{ "Current value: " }</b>
{ counter }
{ *counter }
</p>
</div>
}
Expand Down Expand Up @@ -129,12 +106,7 @@ fn reducer() -> Html {
counter: i32,
}

let (
counter, // the state
// function to update the state
// as the same suggests, it dispatches the values to the reducer function
dispatch
) = use_reducer(
let counter = use_reducer(
// the reducer function
|prev: Rc<CounterState>, action: Action| CounterState {
counter: match action {
Expand All @@ -146,11 +118,14 @@ fn reducer() -> Html {
CounterState { counter: 1 },
);

let double_onclick = {
let dispatch = Rc::clone(&dispatch);
Callback::from(move |_| dispatch(Action::Double))
let double_onclick = {
let counter = counter.clone();
Callback::from(move |_| counter.dispatch(Action::Double))
};
let square_onclick = {
let counter = counter.clone();
Callback::from(move |_| counter.dispatch(Action::Square))
};
let square_onclick = Callback::from(move |_| dispatch(Action::Square));

html! {
<>
Expand All @@ -172,7 +147,7 @@ This is useful for lazy initialization where it is beneficial not to perform exp
computation up-front.

```rust
let (counter, dispatch) = use_reducer_with_init(
let counter = use_reducer_with_init(
// reducer function
|prev: Rc<CounterState>, action: i32| CounterState {
counter: prev.counter + action,
Expand Down
2 changes: 2 additions & 0 deletions packages/yew-functional/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,8 @@ description = "A framework for making client-side single-page apps"
wasm-bindgen-test = "0.3.9"
web-sys = "0.3.36"
yew = { path = "../yew" }
log = "0.4"
wasm-logger = "0.2"
yew-services = { path = "../yew-services" }

[dependencies]
Expand Down
77 changes: 37 additions & 40 deletions packages/yew-functional/src/hooks/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -10,65 +10,62 @@ pub use use_reducer::*;
pub use use_ref::*;
pub use use_state::*;

use crate::CURRENT_HOOK;
use crate::{HookUpdater, CURRENT_HOOK};
use std::cell::RefCell;
use std::ops::DerefMut;
use std::rc::Rc;

pub trait Hook {
fn tear_down(&mut self) {}
}

pub fn use_hook<InternalHookState, HookRunner, R, InitialStateProvider, HookUpdate: 'static>(
hook_runner: HookRunner,
initial_state_producer: InitialStateProvider,
) -> R
where
HookRunner: FnOnce(&mut InternalHookState, Box<dyn Fn(HookUpdate, bool)>) -> R,
InternalHookState: Hook + 'static,
InitialStateProvider: FnOnce() -> InternalHookState,
HookUpdate: FnOnce(&mut InternalHookState) -> bool,
{
/// Low level building block of creating hooks.
///
/// It is used to created the pre-defined primitive hooks.
/// Generally, it isn't needed to create hooks and should be avoided as most custom hooks can be
/// created by combining other hooks as described in [Yew Docs].
///
/// The `initializer` callback is called once to create the initial state of the hook.
/// `runner` callback handles the logic of the hook. It is called when the hook function is called.
/// `destructor`, as the name implies, is called to cleanup the leftovers of the hook.
///
/// See the pre-defined hooks for examples of how to use this function.
///
/// [Yew Docs]: https://yew.rs/docs/en/next/concepts/function-components/custom-hooks
pub fn use_hook<InternalHook: 'static, Output, Tear: FnOnce(&mut InternalHook) + 'static>(
initializer: impl FnOnce() -> InternalHook,
runner: impl FnOnce(&mut InternalHook, HookUpdater) -> Output,
destructor: Tear,
) -> Output {
// Extract current hook
let (hook, process_message) = CURRENT_HOOK.with(|hook_state| {
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(initial_state_producer()));
let initial_state = Rc::new(RefCell::new(initializer()));
hook_state.hooks.push(initial_state.clone());
hook_state.destroy_listeners.push(Box::new(move || {
initial_state.borrow_mut().deref_mut().tear_down();
destructor(initial_state.borrow_mut().deref_mut());
}));
}

let hook = hook_state.hooks[hook_pos].clone();
let hook = hook_state
.hooks
.get(hook_pos)
.expect("Not the same number of hooks. Hooks must not be called conditionally")
.clone();

(hook, hook_state.process_message.clone())
HookUpdater {
hook,
process_message: hook_state.process_message.clone(),
}
});

let hook: Rc<RefCell<InternalHookState>> = hook
.downcast()
.expect("Incompatible hook type. Hooks must always be called in the same order");

let hook_callback = {
let hook = hook.clone();
Box::new(move |update: HookUpdate, post_render| {
let hook = hook.clone();
process_message(
Box::new(move || {
let mut hook = hook.borrow_mut();
update(&mut hook)
}),
post_render,
);
})
};

// 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 = hook.borrow_mut();
hook_runner(&mut hook, hook_callback)
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())
}
Loading

0 comments on commit aa828d7

Please sign in to comment.