1

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)
}};

1 Answer 1

3

You are creating a new array on every mapStateToProps call with myArray: [0, 1, 2]. Because of this, mapStateToProps always returns an object with a new instance of an array.

Redux than does a shallow comparison of the previous props. The references of myArray changed, since oyu created a new one during the call and all cells get udpated. By removing the myArray: [0, 1, 2] line, it works as expected. Move that array into redux, so that you do not generate a new one of every call.

The pure component is also useless, since redux already does a prop comparison and the pure component does the same thing again, so that is wasted since it always has the same result for redux and the pure component and is just more work.

Update: You are still creating a new array on every mapSateToPropsCall with the map function in getMyArray, since map returns a new array. Why do you not make the array static or memoize that, so that you only generate a new one, if needed?

Why do you not change how you save the data so that state.myArray[index] returns the needed array?

And you still can transform the PureCompoennt to Component.

Sign up to request clarification or add additional context in comments.

6 Comments

Thanks! I've updated my sandbox with your suggestion, and have extended it to calculate myArray in the way my real app does. The calculation causes re-renders. I'll update the question with the new sandbox, and would be very grateful if you have any suggestions.
See my anwser ;)
Thanks again. I've added memoization with reselect and re-reselect, see the now updated sandbox, but I don't think it's working and I can't see what I've done wrong. The cache function runs on every update, and the Cells still all re-render. myArray comes out of a database and it wouldn't be trivial to change how it's structured.
Ah! I think I have it working now! I'll update the sandbox and edit to show the new version.
Yes, that's how you do it, You needed to flip the functions in the selector.
|

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.