Skip to content

Commit

Permalink
Add use_callback hook (#2566)
Browse files Browse the repository at this point in the history
* Add use_callback hook

* Simply use_callback
  • Loading branch information
jetli committed Apr 1, 2022
1 parent ea8a530 commit 421b4e1
Show file tree
Hide file tree
Showing 8 changed files with 244 additions and 2 deletions.
2 changes: 2 additions & 0 deletions packages/yew/src/functional/hooks/mod.rs
@@ -1,10 +1,12 @@
mod use_callback;
mod use_context;
mod use_effect;
mod use_memo;
mod use_reducer;
mod use_ref;
mod use_state;

pub use use_callback::*;
pub use use_context::*;
pub use use_effect::*;
pub use use_memo::*;
Expand Down
75 changes: 75 additions & 0 deletions packages/yew/src/functional/hooks/use_callback.rs
@@ -0,0 +1,75 @@
use crate::callback::Callback;
use crate::functional::{hook, use_memo};

/// Get a immutable reference to a memoized `Callback`.
///
/// Memoization means it will only get recreated when provided dependencies update/change.
/// This is useful when passing callbacks to optimized child components that rely on
/// PartialEq to prevent unnecessary renders.
///
/// # Example
///
/// ```rust
/// # use yew::prelude::*;
/// #
/// #[derive(Properties, PartialEq)]
/// pub struct Props {
/// pub callback: Callback<String, String>,
/// }
///
/// #[function_component(MyComponennt)]
/// fn my_component(props: &Props) -> Html {
/// let greeting = props.callback.emit("Yew".to_string());
///
/// html! {
/// <>{ &greeting }</>
/// }
/// }
///
/// #[function_component(UseCallback)]
/// fn callback() -> Html {
/// let counter = use_state(|| 0);
/// let onclick = {
/// let counter = counter.clone();
/// Callback::from(move |_| counter.set(*counter + 1))
/// };
///
/// // This callback depends on (), so it's created only once, then MyComponennt
/// // will be rendered only once even when you click the button mutiple times.
/// let callback = use_callback(
/// move |name| format!("Hello, {}!", name),
/// ()
/// );
///
/// // It can also be used for events.
/// let oncallback = {
/// let counter = counter.clone();
/// use_callback(
/// move |_e| (),
/// counter
/// )
/// };
///
/// html! {
/// <div>
/// <button {onclick}>{ "Increment value" }</button>
/// <button onclick={oncallback}>{ "Callback" }</button>
/// <p>
/// <b>{ "Current value: " }</b>
/// { *counter }
/// </p>
/// <MyComponennt {callback} />
/// </div>
/// }
/// }
/// ```
#[hook]
pub fn use_callback<IN, OUT, F, D>(f: F, deps: D) -> Callback<IN, OUT>
where
IN: 'static,
OUT: 'static,
F: Fn(IN) -> OUT + 'static,
D: PartialEq + 'static,
{
(*use_memo(move |_| Callback::from(f), deps)).clone()
}
30 changes: 28 additions & 2 deletions packages/yew/src/functional/hooks/use_memo.rs
Expand Up @@ -3,9 +3,35 @@ use std::rc::Rc;

use crate::functional::{hook, use_state};

/// Get a immutable reference to a memoized value
/// Get a immutable reference to a memoized value.
///
/// Memoization means it will only get recalculated when provided dependencies update/change
/// Memoization means it will only get recalculated when provided dependencies update/change.
///
/// # Example
///
/// ```rust
/// # use yew::prelude::*;
/// #
/// #[derive(PartialEq, Properties)]
/// pub struct Props {
/// pub step: usize,
/// }
///
/// #[function_component(UseMemo)]
/// fn memo(props: &Props) -> Html {
/// // Will only get recalculated if `props.step` value changes
/// let message = use_memo(
/// |step| format!("{}. Do Some Expensive Calculation", step),
/// props.step
/// );
///
/// html! {
/// <div>
/// <span>{ (*message).clone() }</span>
/// </div>
/// }
/// }
/// ```
#[hook]
pub fn use_memo<T, F, D>(f: F, deps: D) -> Rc<T>
where
Expand Down
71 changes: 71 additions & 0 deletions packages/yew/tests/use_callback.rs
@@ -0,0 +1,71 @@
#![cfg(feature = "wasm_test")]

use std::sync::atomic::{AtomicBool, Ordering};

mod common;

use common::obtain_result;
use gloo::timers::future::sleep;
use std::time::Duration;
use wasm_bindgen_test::*;
use yew::prelude::*;

wasm_bindgen_test::wasm_bindgen_test_configure!(run_in_browser);

#[wasm_bindgen_test]
async fn use_callback_works() {
#[derive(Properties, PartialEq)]
struct Props {
callback: Callback<String, String>,
}

#[function_component(MyComponennt)]
fn my_component(props: &Props) -> Html {
let greeting = props.callback.emit("Yew".to_string());

static CTR: AtomicBool = AtomicBool::new(false);

if CTR.swap(true, Ordering::Relaxed) {
panic!("multiple times rendered!");
}

html! {
<div>
{"The test output is: "}
<div id="result">{&greeting}</div>
{"\n"}
</div>
}
}

#[function_component(UseCallbackComponent)]
fn use_callback_comp() -> Html {
let state = use_state(|| 0);

let callback = use_callback(move |name| format!("Hello, {}!", name), ());

use_effect(move || {
if *state < 5 {
state.set(*state + 1);
}

|| {}
});

html! {
<div>
<MyComponennt {callback} />
</div>
}
}

yew::Renderer::<UseCallbackComponent>::with_root(
gloo_utils::document().get_element_by_id("output").unwrap(),
)
.render();

sleep(Duration::ZERO).await;

let result = obtain_result();
assert_eq!(result.as_str(), "Hello, Yew!");
}
Expand Up @@ -28,6 +28,7 @@ Yew comes with the following predefined Hooks:
- [`use_state`](./use-state.mdx)
- [`use_state_eq`](./use-state.mdx#use_state_eq)
- [`use_memo`](./use-memo.mdx)
- [`use_callback`](./use-callback.mdx)
- [`use_mut_ref`](./use-mut-ref.mdx)
- [`use_node_ref`](./use-node-ref.mdx)
- [`use_reducer`](./use-reducer.mdx)
Expand Down
65 changes: 65 additions & 0 deletions website/docs/concepts/function-components/hooks/use-callback.mdx
@@ -0,0 +1,65 @@
---
title: "use_callback"
---

`use_callback` is used for obtaining an immutable reference to a memoized `Callback`.
Its state persists across renders.
It will be recreated only if any of the dependencies values change.

`use_callback` can be useful when passing callbacks to optimized child components that rely on
PartialEq to prevent unnecessary renders.

```rust
use yew::prelude::*;

#[derive(Properties, PartialEq)]
pub struct Props {
pub callback: Callback<String, String>,
}

#[function_component(MyComponennt)]
fn my_component(props: &Props) -> Html {
let greeting = props.callback.emit("Yew".to_string());

html! {
<>{ &greeting }</>
}
}

#[function_component(UseCallback)]
fn callback() -> Html {
let counter = use_state(|| 0);
let onclick = {
let counter = counter.clone();
Callback::from(move |_| counter.set(*counter + 1))
};

// This callback depends on (), so it's created only once, then MyComponennt
// will be rendered only once even when you click the button mutiple times.
let callback = use_callback(
move |name| format!("Hello, {}!", name),
()
);

// It can also be used for events.
let oncallback = {
let counter = counter.clone();
use_callback(
move |_e| (),
counter
)
};

html! {
<div>
<button {onclick}>{ "Increment value" }</button>
<button onclick={oncallback}>{ "Callback" }</button>
<p>
<b>{ "Current value: " }</b>
{ *counter }
</p>
<MyComponennt {callback} />
</div>
}
}
```
1 change: 1 addition & 0 deletions website/docs/concepts/function-components/state.mdx
Expand Up @@ -13,5 +13,6 @@ This table can be used as a guide when deciding what state storing type fits bes
| [use_reducer](./hooks/use-reducer) | got reduced | component instance |
| [use_reducer_eq](./hooks/use-reducer#use_reducer_eq) | got reduced with diff. value | component instance |
| [use_memo](./hooks/use-memo) | dependencies changed | component instance |
| [use_callback](./hooks/use-callback) | dependencies changed | component instance |
| [use_mut_ref](./hooks/use-mut-ref) | - | component instance |
| a static global variable | - | global, used by all |
1 change: 1 addition & 0 deletions website/sidebars/docs.js
Expand Up @@ -62,6 +62,7 @@ module.exports = {
"concepts/function-components/hooks/use-node-ref",
"concepts/function-components/hooks/use-effect",
"concepts/function-components/hooks/use-memo",
"concepts/function-components/hooks/use-callback",
"concepts/function-components/hooks/use-context",
"concepts/function-components/hooks/custom-hooks",
],
Expand Down

1 comment on commit 421b4e1

@github-actions
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yew master branch benchmarks (Lower is better)

Benchmark suite Current: 421b4e1 Previous: ea8a530 Ratio
yew-struct-keyed 01_run1k 179.1905 237.946 0.75
yew-struct-keyed 02_replace1k 180.5605 231.9485 0.78
yew-struct-keyed 03_update10th1k_x16 324.7545 402.2355 0.81
yew-struct-keyed 04_select1k 63.3815 73.894 0.86
yew-struct-keyed 05_swap1k 77.0505 94.2395 0.82
yew-struct-keyed 06_remove-one-1k 28.3335 30.8645 0.92
yew-struct-keyed 07_create10k 2279.2725 2680.2865 0.85
yew-struct-keyed 08_create1k-after1k_x2 411.3745 498.7965 0.82
yew-struct-keyed 09_clear1k_x8 182.642 241.5745 0.76
yew-struct-keyed 21_ready-memory 1.4005584716796875 1.2447853088378906 1.13
yew-struct-keyed 22_run-memory 1.6612091064453125 1.45648193359375 1.14
yew-struct-keyed 23_update5-memory 1.70111083984375 1.5057640075683594 1.13
yew-struct-keyed 24_run5-memory 1.717632293701172 1.5095291137695312 1.14
yew-struct-keyed 25_run-clear-memory 1.421672821044922 1.1240959167480469 1.26
yew-struct-keyed 31_startup-ci 1730.362 1732.398 1.00
yew-struct-keyed 32_startup-bt 27.70800000000001 37.907999999999994 0.73
yew-struct-keyed 34_startup-totalbytes 330.546875 330.5556640625 1.00

This comment was automatically generated by workflow using github-action-benchmark.

Please sign in to comment.