From a0406ae72760408a46a81d16c761691acb60507c Mon Sep 17 00:00:00 2001 From: Pascal Sommer Date: Sun, 16 Nov 2025 14:23:46 +0100 Subject: [PATCH 1/5] add failing test --- packages/yew/tests/use_reducer.rs | 91 +++++++++++++++++++++++++++++++ 1 file changed, 91 insertions(+) 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"); +} From d5d40bb73d18b2756e649b1f483f8005ec46a9ef Mon Sep 17 00:00:00 2001 From: Pascal Sommer Date: Sun, 16 Nov 2025 14:27:22 +0100 Subject: [PATCH 2/5] avoid rerender in use_reducer when Rc is reused --- packages/yew/src/functional/hooks/use_reducer.rs | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/packages/yew/src/functional/hooks/use_reducer.rs b/packages/yew/src/functional/hooks/use_reducer.rs index db81c38179d..23a41608460 100644 --- a/packages/yew/src/functional/hooks/use_reducer.rs +++ b/packages/yew/src/functional/hooks/use_reducer.rs @@ -224,7 +224,12 @@ where let should_render_fn = should_render_fn.clone(); let mut val = val.borrow_mut(); let next_val = (*val).clone().reduce(action); - let should_render = should_render_fn(&next_val, &val); + + // Check if the reduce action just returned the same `Rc` again + // instead of producing a new one. + let rc_was_reused = Rc::ptr_eq(&val, &next_val); + + let should_render = !rc_was_reused && should_render_fn(&next_val, &val); *val = next_val; should_render From cff516e499719883e22e578b1c9b31e9f7276358 Mon Sep 17 00:00:00 2001 From: Pascal Sommer Date: Tue, 18 Nov 2025 21:47:23 +0100 Subject: [PATCH 3/5] add warning comment about assumptions --- packages/yew/src/functional/hooks/use_reducer.rs | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/packages/yew/src/functional/hooks/use_reducer.rs b/packages/yew/src/functional/hooks/use_reducer.rs index 23a41608460..9611205094a 100644 --- a/packages/yew/src/functional/hooks/use_reducer.rs +++ b/packages/yew/src/functional/hooks/use_reducer.rs @@ -225,8 +225,14 @@ where let mut val = val.borrow_mut(); let next_val = (*val).clone().reduce(action); - // Check if the reduce action just returned the same `Rc` again - // instead of producing a new one. + // Check if the reduce action just returned the same `Rc` again instead of producing + // a new one. + // NOTE: here we make the assumption that an unchanged address implies that the + // "identity" of the `Rc` is unchanged. This assumption is valid here because we + // still keep the old Rc around. But if we were to instead move the old Rc into + // the `reduce` function, then the address could be reused and the object inside + // the Rc might be different. The `rc_was_reused` variable is thus only meaningful + // as long as we use a `clone` before `reduce`. let rc_was_reused = Rc::ptr_eq(&val, &next_val); let should_render = !rc_was_reused && should_render_fn(&next_val, &val); From 11a464021339e1f8a7def1f6f000ad5a327ccdfe Mon Sep 17 00:00:00 2001 From: Pascal Sommer Date: Tue, 18 Nov 2025 21:58:53 +0100 Subject: [PATCH 4/5] update use_reducer docs --- packages/yew/src/functional/hooks/use_reducer.rs | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/packages/yew/src/functional/hooks/use_reducer.rs b/packages/yew/src/functional/hooks/use_reducer.rs index 9611205094a..6368cc4f622 100644 --- a/packages/yew/src/functional/hooks/use_reducer.rs +++ b/packages/yew/src/functional/hooks/use_reducer.rs @@ -275,8 +275,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 From 5e3dadf23ec0190688b2aeb788aadf91a6f9dee3 Mon Sep 17 00:00:00 2001 From: Pascal Sommer Date: Tue, 18 Nov 2025 23:33:13 +0100 Subject: [PATCH 5/5] move check to should_render_fn --- .../yew/src/functional/hooks/use_reducer.rs | 26 +++++++------------ 1 file changed, 10 insertions(+), 16 deletions(-) diff --git a/packages/yew/src/functional/hooks/use_reducer.rs b/packages/yew/src/functional/hooks/use_reducer.rs index 6368cc4f622..fbc819fb5e7 100644 --- a/packages/yew/src/functional/hooks/use_reducer.rs +++ b/packages/yew/src/functional/hooks/use_reducer.rs @@ -224,18 +224,7 @@ where let should_render_fn = should_render_fn.clone(); let mut val = val.borrow_mut(); let next_val = (*val).clone().reduce(action); - - // Check if the reduce action just returned the same `Rc` again instead of producing - // a new one. - // NOTE: here we make the assumption that an unchanged address implies that the - // "identity" of the `Rc` is unchanged. This assumption is valid here because we - // still keep the old Rc around. But if we were to instead move the old Rc into - // the `reduce` function, then the address could be reused and the object inside - // the Rc might be different. The `rc_was_reused` variable is thus only meaningful - // as long as we use a `clone` before `reduce`. - let rc_was_reused = Rc::ptr_eq(&val, &next_val); - - let should_render = !rc_was_reused && should_render_fn(&next_val, &val); + let should_render = should_render_fn(&next_val, &val); *val = next_val; should_render @@ -277,8 +266,8 @@ where /// /// 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. +/// 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`]. @@ -366,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`. @@ -379,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) }