diff --git a/packages/yew/src/functional/hooks/mod.rs b/packages/yew/src/functional/hooks/mod.rs index e45c93e423e..c751e9666f5 100644 --- a/packages/yew/src/functional/hooks/mod.rs +++ b/packages/yew/src/functional/hooks/mod.rs @@ -1,3 +1,4 @@ +mod use_callback; mod use_context; mod use_effect; mod use_memo; @@ -5,6 +6,7 @@ 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::*; diff --git a/packages/yew/src/functional/hooks/use_callback.rs b/packages/yew/src/functional/hooks/use_callback.rs new file mode 100644 index 00000000000..3fc015bf64b --- /dev/null +++ b/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, +/// } +/// +/// #[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! { +///
+/// +/// +///

+/// { "Current value: " } +/// { *counter } +///

+/// +///
+/// } +/// } +/// ``` +#[hook] +pub fn use_callback(f: F, deps: D) -> Callback +where + IN: 'static, + OUT: 'static, + F: Fn(IN) -> OUT + 'static, + D: PartialEq + 'static, +{ + (*use_memo(move |_| Callback::from(f), deps)).clone() +} diff --git a/packages/yew/src/functional/hooks/use_memo.rs b/packages/yew/src/functional/hooks/use_memo.rs index f7538c23e64..a97ca2421da 100644 --- a/packages/yew/src/functional/hooks/use_memo.rs +++ b/packages/yew/src/functional/hooks/use_memo.rs @@ -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! { +///
+/// { (*message).clone() } +///
+/// } +/// } +/// ``` #[hook] pub fn use_memo(f: F, deps: D) -> Rc where diff --git a/packages/yew/tests/use_callback.rs b/packages/yew/tests/use_callback.rs new file mode 100644 index 00000000000..b4fe635d8e1 --- /dev/null +++ b/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, + } + + #[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! { +
+ {"The test output is: "} +
{&greeting}
+ {"\n"} +
+ } + } + + #[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! { +
+ +
+ } + } + + yew::Renderer::::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!"); +} diff --git a/website/docs/concepts/function-components/hooks/introduction.mdx b/website/docs/concepts/function-components/hooks/introduction.mdx index c6fb31fc4c9..bba9113b5a1 100644 --- a/website/docs/concepts/function-components/hooks/introduction.mdx +++ b/website/docs/concepts/function-components/hooks/introduction.mdx @@ -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) diff --git a/website/docs/concepts/function-components/hooks/use-callback.mdx b/website/docs/concepts/function-components/hooks/use-callback.mdx new file mode 100644 index 00000000000..11f20e9e0bd --- /dev/null +++ b/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, +} + +#[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! { +
+ + +

+ { "Current value: " } + { *counter } +

+ +
+ } +} +``` diff --git a/website/docs/concepts/function-components/state.mdx b/website/docs/concepts/function-components/state.mdx index 3211f5419f0..b4f99d737b7 100644 --- a/website/docs/concepts/function-components/state.mdx +++ b/website/docs/concepts/function-components/state.mdx @@ -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 | diff --git a/website/sidebars/docs.js b/website/sidebars/docs.js index 50f5fe0c003..31846753d00 100644 --- a/website/sidebars/docs.js +++ b/website/sidebars/docs.js @@ -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", ],