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

Upgraded Hook API (2) #1780

Merged
merged 14 commits into from
May 5, 2021
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