Nicolas S.
Nicolas S.

Reputation: 173

Same values for this.props, nextProp and nextState leads to bad rendering

I would like to update a child component only if his value has changed. I have this code :

<Container fluid>
                <Row>
                    <Col md={2}>NameMonth + year</Col>
                    <Col md={10}>
                        <div className="vacationMonthDiv text-left">
                            {arrDays.map((item, index) => { // arrDays contains a list of moment of the actual month generated
                                return <Day day={this.state.calendar[numDays[index]]} mouseDown={this.clickDownVacation} key={"fullDay" + item.toString()} /> //state.calendar contains a list of object with a moment, 2 booleans (morning and afternoon) and an id
                            })}
                        </div>
                    </Col>
                </Row>
            </Container>

Day component (Using PureComponent but I tried to use shouldComponentUpdate() before (put the code in comment) :

export default class Day extends PureComponent {

/*shouldComponentUpdate(nextProps, nextState) {
    return shallowCompare(this, nextProps, nextState);
}*/

render() {
    this.props.day.date.locale('fr');
    var jour = this.props.day.date.format("ddd");
    var jourSemaine = this.props.day.date.format("DD");
    jour = jour.charAt(0).toUpperCase() + jour.slice(1);
    var isoDay = this.props.day.date.isoWeekday();
    return (
        <div className={isoDay === 7 ? "inline-block vacationLastOfWeek" : "inline-block"} key={"fullDay" + this.props.day.id} >
        <div className="vacationDayName text-center" key={"nameDay" + this.props.day.id}>{jour}</div>
            <div className="vacationDayNumber text-center" key={"numDay" + this.props.day.id}>{jourSemaine}</div>
            <div className={isoDay >= 6 || this.props.day.date.isFerie() ? "vacationHalfDay vacationMorning text-center cursor-normal vacationPublicHoliday" : "vacationHalfDay vacationMorning text-center cursor-pointer"} onMouseDown={() => this.props.mouseDown(this.props.day.id, true)} >
                {this.props.day.isMorningSelected ? <MaterialIcon icon="close" color="red" /> : <div> {isoDay >= 6 || this.props.day.date.isFerie() ? "W" : "M"}</div>}
        </div>
            <div className={isoDay >= 6 || this.props.day.date.isFerie() ? "vacationHalfDay vacationAfternoon text-center cursor-normal vacationPublicHoliday" : "vacationHalfDay vacationAfternoon text-center cursor-pointer"} onMouseDown={() => this.props.mouseDown(this.props.day.id, false)} >
                {this.props.day.isAfternoonSelected ? <MaterialIcon icon="close" color="red" /> : <div>{isoDay >= 6 || this.props.day.date.isFerie() ? "W" : "A"}</div>}
        </div>
    </div>
    );
}
}

Now the generated code on day is working . As I am generating a full year, react takes a lot of time to re-generate every Day component. Here's an example of a generated month :

A generated month (january 2018)

When a user click on a morning (M) or an afternoon (A), a icon appear to consay that it has been checked. However, when the user click, every component is re-rendering, which leads react to be slow.

Doing some test, I discovered that in my Day component when I used shouldcomponentupdate(), the nextState, nextProp and the actual state are exactly the same, which leads comparison to be false.

How can I compare these values to activate rendering only if a value has changed ?

EDIT : Here's clickDownVacation :

clickDownVacation = (index, isMorning) => {
    var array = this.state.calendar;
    if (!array[index].date.isFerie() && array[index].date.isoWeekday() < 6) {
    if (isMorning) array[index].isMorningSelected = !array[index].isMorningSelected;
    else array[index].isAfternoonSelected = !array[index].isAfternoonSelected;

    this.setState({
        calendar: array
        });
    }
}

EDIT 2 : Here's a reproduction : Codebox

Upvotes: 1

Views: 170

Answers (1)

DoXicK
DoXicK

Reputation: 4812

When you change a value within an object, the reference to that object doesn't change. React does shallow comparison of its props to see if it should re-render. It only checks if the prop changed, not if something in the prop changed.

For an array or an object, it only checks 'is this the same array or object', not 'does this array/object contain the same values'

For your code, you'll have to change clickDownVacation:

clickDownVacation = (index, isMorning) => {
  let array = ...this.state.calendar;
  let day = array[index]
  if (!day.date.isFerie() && day.date.isoWeekday() < 6) {
    array = [...array] // create a new array, containing the old values of array
    if (isMorning) {
      day = { // create a new 'day' object
        ...day, // copy all the old values of day into this new object
        isMorningSelected: !day.isMorningSelected;
      }
    }
    else {
      day = { // create a new 'day' object
        ...day, // copy all the old values of day into this new object
        isAfternoonSelected: !day.isAfternoonSelected;
      }
    }
    array[index] = day
    this.setState({
      calendar: array
      });
    }
  }
}

Since both the "array" is a new array and the "day" is a new object, react can compare this against the old values and will see that the references are different, and thus re-render

A good guide about 'immutability' which explains this issue is https://daveceddia.com/react-redux-immutability-guide/

Upvotes: 1

Related Questions