diff --git a/packages/yew/src/functional/hooks/use_reducer.rs b/packages/yew/src/functional/hooks/use_reducer.rs index db81c38179d..fbc819fb5e7 100644 --- a/packages/yew/src/functional/hooks/use_reducer.rs +++ b/packages/yew/src/functional/hooks/use_reducer.rs @@ -264,8 +264,13 @@ where /// implement a `Reducible` trait which defines the associated `Action` type and a /// reducer function. /// -/// This hook will always trigger a re-render upon receiving an action. See -/// [`use_reducer_eq`] if you want the component to only re-render when the state changes. +/// This hook will trigger a re-render whenever the reducer function produces a new `Rc` value upon +/// receiving an action. If the reducer function simply returns the original `Rc` then the component +/// will not re-render. See [`use_reducer_eq`] if you want the component to first compare the old +/// and new state and only re-render when the state actually changes. +/// +/// To cause a re-render even if the reducer function returns the same `Rc`, take a look at +/// [`use_force_update`]. /// /// # Example /// ```rust @@ -350,7 +355,7 @@ where T: Reducible + 'static, F: FnOnce() -> T, { - use_reducer_base(init_fn, |_, _| true) + use_reducer_base(init_fn, |a, b| !address_eq(a, b)) } /// [`use_reducer`] but only re-renders when `prev_state != next_state`. @@ -363,5 +368,10 @@ where T: Reducible + PartialEq + 'static, F: FnOnce() -> T, { - use_reducer_base(init_fn, T::ne) + use_reducer_base(init_fn, |a, b| !address_eq(a, b) && a != b) +} + +/// Check if two references point to the same address. +fn address_eq(a: &T, b: &T) -> bool { + std::ptr::eq(a as *const T, b as *const T) } diff --git a/packages/yew/tests/use_reducer.rs b/packages/yew/tests/use_reducer.rs index 2394f49a13c..53191c024a6 100644 --- a/packages/yew/tests/use_reducer.rs +++ b/packages/yew/tests/use_reducer.rs @@ -162,3 +162,94 @@ async fn use_reducer_eq_works() { let result = obtain_result(); assert_eq!(result.as_str(), "3"); } + +enum SometimesChangeAction { + /// If this action is sent, the state will remain the same + Keep, + /// If this action is sent, the state will change + Change, +} + +/// A state that does not implement PartialEq +#[derive(Clone)] +struct SometimesChangingState { + value: i32, +} + +impl Reducible for SometimesChangingState { + type Action = SometimesChangeAction; + + fn reduce(self: Rc, action: Self::Action) -> Rc { + use SometimesChangeAction::*; + match action { + Keep => self, + Change => { + let mut self_: Self = (*self).clone(); + self_.value += 1; + self_.into() + } + } + } +} + +#[wasm_bindgen_test] +async fn use_reducer_does_not_rerender_when_rc_is_reused() { + #[component(UseReducerComponent)] + fn use_reducer_comp() -> Html { + let state = use_reducer(|| SometimesChangingState { value: 0 }); + let render_count = use_mut_ref(|| 0); + + let render_count = { + let mut render_count = render_count.borrow_mut(); + *render_count += 1; + + *render_count + }; + + let keep_state = { + let state = state.clone(); + Callback::from(move |_| state.dispatch(SometimesChangeAction::Keep)) + }; + + let change_state = Callback::from(move |_| state.dispatch(SometimesChangeAction::Change)); + + html! { + <> +
+ {"This component has been rendered: "}{render_count}{" Time(s)."} +
+ + + + } + } + + yew::Renderer::::with_root( + document().get_element_by_id("output").unwrap(), + ) + .render(); + sleep(Duration::ZERO).await; + + let result = obtain_result(); + assert_eq!(result.as_str(), "1"); + + document() + .get_element_by_id("change-state") + .unwrap() + .unchecked_into::() + .click(); + sleep(Duration::ZERO).await; + + let result = obtain_result(); + assert_eq!(result.as_str(), "2"); + + document() + .get_element_by_id("keep-state") + .unwrap() + .unchecked_into::() + .click(); + sleep(Duration::ZERO).await; + + let result = obtain_result(); + assert_eq!(result.as_str(), "2"); +}