Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
18 changes: 14 additions & 4 deletions packages/yew/src/functional/hooks/use_reducer.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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`.
Expand All @@ -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<T>(a: &T, b: &T) -> bool {
std::ptr::eq(a as *const T, b as *const T)
}
91 changes: 91 additions & 0 deletions packages/yew/tests/use_reducer.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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<Self>, action: Self::Action) -> Rc<Self> {
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! {
<>
<div>
{"This component has been rendered: "}<span id="result">{render_count}</span>{" Time(s)."}
</div>
<button onclick={keep_state} id="keep-state">{"Keep State"}</button>
<button onclick={change_state} id="change-state">{"Change State"}</button>
</>
}
}

yew::Renderer::<UseReducerComponent>::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::<HtmlElement>()
.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::<HtmlElement>()
.click();
sleep(Duration::ZERO).await;

let result = obtain_result();
assert_eq!(result.as_str(), "2");
}
Loading