I have a React / Redux app that renders a grid of PureComponents. I want the components to only re-render when a prop value has changed, but in fact all components re-render on any update to store. The issue appears to be caused by an array property.
I've reproduced the issue in this sandbox. In this minimal example, two instances of the Cell component are rendered. Each displays a value that is separately recorded in the store, and can be incremented or decremented separately with a button.
Cell.js
import React, { PureComponent } from "react";
import { connect } from "react-redux";
import { incrementAction, decreaseAction } from "./actions";
class Cell extends PureComponent {
render() {
const { index, value, incrementAction, decreaseAction } = this.props;
console.log("render cell with index", index);
return (
<div>
<h1>{value}</h1>
<button onClick={incrementAction}>increment</button>
<button onClick={decreaseAction}>decrease</button>
</div>
);
}
}
const mapStateToProps = (state, ownProps) => ({
value: ownProps.index === 1 ? state.value1 : state.value2,
myArray: [0, 1, 2]
});
const mapDispatchToProps = (dispatch, ownProps) => ({
incrementAction: () => dispatch(incrementAction(ownProps.index)),
decreaseAction: () => dispatch(decreaseAction(ownProps.index))
});
export default connect(
mapStateToProps,
mapDispatchToProps
)(Cell);
If you check the console, you should see that when you click one button, both Cells re-render.
If you comment out the myArray prop in mapStateToProps, the behaviour changes so that only the cell you've clicked re-renders.
So it appears that PureComponent is re-rendering on any change to the store, because of the static array property.
In my real app the array would come from the store also, and Cell should re-render if an array value changes, but the sandbox example shows how even a static array property triggers re-render.
Is there any way to provide an array prop to a PureComponent and have it only re-render when a prop changes? Thank you.
Edit: I've updated the sandbox to move myArray into the store as suggested by Domino987 and to add a function to calculate the sub-array required by the Cell component - this is what my real app does. I've added memoization with reselect and re-reselect, and I've made it a functional component instead of PureComponent. As far as I can see, this is now working - only one Cell re-renders when you click a button. Yay!
In actions.js:
import createCachedSelector from "re-reselect";
export function getMyArray(state, index) {
console.log("getMyArrayCached for index", index);
return state.myArray;
}
export function getIndex(state, index) {
return index;
}
export const getMyArrayCached = createCachedSelector(
getMyArray,
getIndex,
(myArray, index) =>
myArray.map(elm => {
return elm[index - 1];
})
)((_state_, index) => index);
in reducer.js:
const initialState = {
value1: 0,
value2: 0,
myArray: [[1, 2], [1, 2], [1, 2]]
};
in Cell.js:
const mapStateToProps = (state, ownProps) => {
const value = ownProps.index === 1 ? state.value1 : state.value2;
return {
value,
myArray: getMyArrayCached(state, ownProps.index)
}};