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

Add use_callback hook #2566

Merged
merged 2 commits into from
Apr 1, 2022
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions packages/yew/src/functional/hooks/mod.rs
Original file line number Diff line number Diff line change
@@ -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
97 changes: 97 additions & 0 deletions packages/yew/src/functional/hooks/use_callback.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,97 @@
use std::cell::RefCell;

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

/// 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,
{
let callback = use_state(|| -> RefCell<Option<Callback<IN, OUT>>> { RefCell::new(None) });
Copy link
Member

Choose a reason for hiding this comment

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

Unless I am missing something, this can simply be
(*use_memo(move |_| Callback::from(f), deps)).clone()?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

You are correct, updated

Copy link
Contributor Author

Choose a reason for hiding this comment

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

then I'm wondering whether we need to add use_callback at all? but React uses useCallback a lot for performance.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

another issue, the array of dependencies is not passed as arguments to the callback, if callback wants to consume deps, that might not be convenient.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

let oncallback = {
    let counter = counter.clone();
    let counter2 = counter.clone();
    use_callback(
        move |e| *counter2,
        counter
    )
};

vs

let oncallback = {
    let counter = counter.clone();
    use_memo(
        move |counter| {
            Callback::from(move |e| **counter)
        },
        counter
    )
};

Copy link
Contributor Author

Choose a reason for hiding this comment

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

@futursolo any advice?

Copy link
Member

Choose a reason for hiding this comment

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

I believe that React has a separate useCallback primarily due to useMemo(() => () => {}, []) looks a little bit complicated. I think in this case, if use_callback manages deps for you, it might still be worth it.

So one can do something like:

use_callback(move |deps| move |in_type| {}, deps);
// or have some magic by wrapping it in a custom closure.
use_callback(|deps, in_type| {}, deps);

Copy link
Contributor Author

Choose a reason for hiding this comment

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

use_callback(|deps, in_type| {}, deps); looks interesting, I'll have a try.

let last_deps = use_state(|| -> RefCell<Option<D>> { RefCell::new(None) });

let mut callback = callback.borrow_mut();
let mut last_deps = last_deps.borrow_mut();

match (
callback.as_ref(),
last_deps.as_ref().and_then(|m| (m != &deps).then(|| ())),
) {
// Previous callback exists and last_deps == deps
(Some(m), None) => m.clone(),
_ => {
let new_callback = Callback::from(f);
*last_deps = Some(deps);

*callback = Some(new_callback.clone());

new_callback
}
}
}
30 changes: 28 additions & 2 deletions packages/yew/src/functional/hooks/use_memo.rs
Original file line number Diff line number Diff line change
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
Original file line number Diff line number Diff line change
@@ -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!");
}
Original file line number Diff line number Diff line change
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
Original file line number Diff line number Diff line change
@@ -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
Original file line number Diff line number Diff line change
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
Original file line number Diff line number Diff line change
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