-
-
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
Add use_callback hook #2566
Merged
Merged
Add use_callback hook #2566
Changes from 1 commit
Commits
Show all changes
2 commits
Select commit
Hold shift + click to select a range
File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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) }); | ||
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 | ||
} | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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!"); | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
65 changes: 65 additions & 0 deletions
65
website/docs/concepts/function-components/hooks/use-callback.mdx
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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> | ||
} | ||
} | ||
``` |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
There was a problem hiding this comment.
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()
?There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
You are correct, updated
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
vs
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@futursolo any advice?
There was a problem hiding this comment.
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 touseMemo(() => () => {}, [])
looks a little bit complicated. I think in this case, ifuse_callback
managesdeps
for you, it might still be worth it.So one can do something like:
There was a problem hiding this comment.
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.