we can use Immer to write better reducer while still maintain immutability https://www.smashingmagazine.com/2020/06/better-reducers-with-immer/
below is the code, ComponentA invoke the dispatch() call and ComponentB uses useSelector to access the state and display it
const ComponentA = () => {
const dispatch = useDispatch();
return (
<div>
<ComponentB />
<button onClick={() => dispatch({ type: "Add", payload: { id: 4, name: "table" } })}>Add Product</button>
</div>
);
}
const ComponentB = () => {
const products = useSelector(state => state.products); // state.products is an array [{ id, name}, ...]
return (
<div>
<h1>{products.reduce((a, b) => a + " " + b.name, "")}</h1>
</div>
);
}
and the reducer is
import { produce } from "immer";
export const initialData = {
products: [
{ id: 1, name: "Shoes" }, ...],
suppliers: [
{ id: 1, name: "Zoom Shoes" },...]
}
const reducer = produce((draft = initialData, action) => {
switch (action.type) {
case "Add":
draft.products.push(action.payload);
break;
default:
break;
}
return draft;
});
My understanding in Immer is: Immer is copy-on-write, if you have 3 items in an array draft = [{name: "shoe" }, {name: "hat"}, {name: "gloves"}], and if you modify the second item as draft[1].name = "shirt", Immer will create a new obejct for the second item while the first and third item remain the same, same as the array itself.
So I expect the page won't update to display new item when clicking the button even though state is changed ComponentB is listening on the change on state.products.I know it sounds strange, but according to the React Redux official guide:
When an action is dispatched, useSelector() will do a reference comparison of the previous selector result value and the current result value. If they are different, the component will be forced to re-render. If they are the same, the component will not re-render. useSelector() uses strict === reference equality checks by default, not shallow equality
so if the reference of array state.products is still the same (no new array instance created), then useSelector in ComponentB should think there is no change on the state.products, so it won't rerender.
But when I run the app I found both of ComponentA and ComponentB are re-rendered, why ComponentA was re-rendered? I don't think calling useDispatch() cause re-renders. So the re-render of ComponentA can only be from one of these condition below:
a. Calling useDispatch() automatically trigger a re-render on the calling component
b. Immer create a new array when a new item appended to it
I don't think it is b, because if Immer create a new array, componentA should not be re-rendered.
No. This is trivially testable.
Given the code for components A and B:
App:
Reducer/store:
This is the output when running the app and pressing the "Add Product" three times:
The first two A and B renders are the initial render and the remount render the
React.StrictModecomponent does. Following that are the three expected B-only console logs from only component B being rerendered.Demo Code