Skip to content

Commit b5c91a5

Browse files
authored
perf: make use_reducer skip re-rendering for the same Rc (#3945)
1 parent 90e8230 commit b5c91a5

File tree

2 files changed

+105
-4
lines changed

2 files changed

+105
-4
lines changed

packages/yew/src/functional/hooks/use_reducer.rs

Lines changed: 14 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -319,8 +319,13 @@ where
319319
/// implement a `Reducible` trait which defines the associated `Action` type and a
320320
/// reducer function.
321321
///
322-
/// This hook will always trigger a re-render upon receiving an action. See
323-
/// [`use_reducer_eq`] if you want the component to only re-render when the state changes.
322+
/// This hook will trigger a re-render whenever the reducer function produces a new `Rc` value upon
323+
/// receiving an action. If the reducer function simply returns the original `Rc` then the component
324+
/// will not re-render. See [`use_reducer_eq`] if you want the component to first compare the old
325+
/// and new state and only re-render when the state actually changes.
326+
///
327+
/// To cause a re-render even if the reducer function returns the same `Rc`, take a look at
328+
/// [`use_force_update`].
324329
///
325330
/// # Example
326331
/// ```rust
@@ -405,7 +410,7 @@ where
405410
T: Reducible + 'static,
406411
F: FnOnce() -> T,
407412
{
408-
use_reducer_base(init_fn, |_, _| true)
413+
use_reducer_base(init_fn, |a, b| !address_eq(a, b))
409414
}
410415

411416
/// [`use_reducer`] but only re-renders when `prev_state != next_state`.
@@ -418,5 +423,10 @@ where
418423
T: Reducible + PartialEq + 'static,
419424
F: FnOnce() -> T,
420425
{
421-
use_reducer_base(init_fn, T::ne)
426+
use_reducer_base(init_fn, |a, b| !address_eq(a, b) && a != b)
427+
}
428+
429+
/// Check if two references point to the same address.
430+
fn address_eq<T>(a: &T, b: &T) -> bool {
431+
std::ptr::eq(a as *const T, b as *const T)
422432
}

packages/yew/tests/use_reducer.rs

Lines changed: 91 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -162,3 +162,94 @@ async fn use_reducer_eq_works() {
162162
let result = obtain_result();
163163
assert_eq!(result.as_str(), "3");
164164
}
165+
166+
enum SometimesChangeAction {
167+
/// If this action is sent, the state will remain the same
168+
Keep,
169+
/// If this action is sent, the state will change
170+
Change,
171+
}
172+
173+
/// A state that does not implement PartialEq
174+
#[derive(Clone)]
175+
struct SometimesChangingState {
176+
value: i32,
177+
}
178+
179+
impl Reducible for SometimesChangingState {
180+
type Action = SometimesChangeAction;
181+
182+
fn reduce(self: Rc<Self>, action: Self::Action) -> Rc<Self> {
183+
use SometimesChangeAction::*;
184+
match action {
185+
Keep => self,
186+
Change => {
187+
let mut self_: Self = (*self).clone();
188+
self_.value += 1;
189+
self_.into()
190+
}
191+
}
192+
}
193+
}
194+
195+
#[wasm_bindgen_test]
196+
async fn use_reducer_does_not_rerender_when_rc_is_reused() {
197+
#[component(UseReducerComponent)]
198+
fn use_reducer_comp() -> Html {
199+
let state = use_reducer(|| SometimesChangingState { value: 0 });
200+
let render_count = use_mut_ref(|| 0);
201+
202+
let render_count = {
203+
let mut render_count = render_count.borrow_mut();
204+
*render_count += 1;
205+
206+
*render_count
207+
};
208+
209+
let keep_state = {
210+
let state = state.clone();
211+
Callback::from(move |_| state.dispatch(SometimesChangeAction::Keep))
212+
};
213+
214+
let change_state = Callback::from(move |_| state.dispatch(SometimesChangeAction::Change));
215+
216+
html! {
217+
<>
218+
<div>
219+
{"This component has been rendered: "}<span id="result">{render_count}</span>{" Time(s)."}
220+
</div>
221+
<button onclick={keep_state} id="keep-state">{"Keep State"}</button>
222+
<button onclick={change_state} id="change-state">{"Change State"}</button>
223+
</>
224+
}
225+
}
226+
227+
yew::Renderer::<UseReducerComponent>::with_root(
228+
document().get_element_by_id("output").unwrap(),
229+
)
230+
.render();
231+
sleep(Duration::ZERO).await;
232+
233+
let result = obtain_result();
234+
assert_eq!(result.as_str(), "1");
235+
236+
document()
237+
.get_element_by_id("change-state")
238+
.unwrap()
239+
.unchecked_into::<HtmlElement>()
240+
.click();
241+
sleep(Duration::ZERO).await;
242+
243+
let result = obtain_result();
244+
assert_eq!(result.as_str(), "2");
245+
246+
document()
247+
.get_element_by_id("keep-state")
248+
.unwrap()
249+
.unchecked_into::<HtmlElement>()
250+
.click();
251+
sleep(Duration::ZERO).await;
252+
253+
let result = obtain_result();
254+
assert_eq!(result.as_str(), "2");
255+
}

0 commit comments

Comments
 (0)