React-redux: Issue with same props in shouldComponentUpdate

Created on 9 Aug 2015  路  12Comments  路  Source: reduxjs/react-redux

I'm having an issue with Redux, when I try to implement shouldComponentUpdate for a dumb component, this.props already holds the same value as nextProps. I read this was an issue linked with Flux architectures which I'm using Redux for, but I don't understand why this would happen. Is there any way around it?

question

Most helpful comment

Basically my prop was mutable so as soon as it was mutated, both the previous and next props actually pointed to the same object, having the same values. I switched to using Immutable.js for objects

All 12 comments

I'm not sure what you mean. If you don't mutate Redux state and always return the new state (that's how you should do in Redux), you shouldn't have such problem. Can you show your code?

I think that might be the issue yes, some object I'm mutating somewhere in the reducer. Just wanted to open an issue ask if that was something you heard about that could be caused by Redux or if it were more an userland error but I assumed it would be the latter yes. Sorry for bothering, will make sure everything is tight and reopen if really I found a bug. :p

Was indeed an immutability issue, rewriting my reducer fixed the issue.

Hi 锛孖 had the same problem. Can you tell me the solution or the reason

+1

Basically my prop was mutable so as soon as it was mutated, both the previous and next props actually pointed to the same object, having the same values. I switched to using Immutable.js for objects

Thanks Anahkiasen. It turns out I too was mutating and returning the object representing the Redux store without realising it.

Redux requires a new object to be returned in the reducer for the shouldComponentUpdate function to show a difference between nextProps and this.props.
I think this.props must directly reference the object returned by the reducer meaning any mutation is already present in the shouldComponentUpdate.

In my case, I was using Object.assign incorrectly in my reducer and returning the original object (instead of creating a new object etc).

In the following example I assumed y was a new object and 'x === y' to return false:

var x = {a: 1};
var y = Object.assign(x, {a: 1});

x === y;  //true

To create a new object, you must ensure the first argument in assign is an empty/or new object:

y = Object.assign({}, x, {a: 1});

x === y;  //false

Yep, there's a very good reason why this is in the Redux FAQ: http://redux.js.org/docs/FAQ.html#react-not-rerendering .

Seriously, I think every single time I've seen this question, the cause was mutation in a reducer.

I am having the same problem, and I've pinned down that it's state being mutated in the reducer, but I can't figure out how it's being mutated. Would anyone mind giving me a hand?

Below is the reducer, and it appears that this is the line that is mutating the state:

newState[action.schedule][action.iterator].shortname = action.answer;

I'm guessing the issue is too deeply nested data. Looking at the console.logs I get LOG2 is false, LOG3 reflects the pre-updated state, but LOG4 reflects the updated state, not pre-updated as it should. LOG5 and beyond reflect the updated state.

Is it possibly something with the spread operator in the top (...state) ? I thought that was it and maybe just had an issue with Babel but I get the same error when I use Object.assign({}, state). I also tried using kolodny/immutability-helper and had the same issue.

Any help would be greatly appreciated!

This is the reducer. It's actually a subreducer, only handling a section of state with multiple insurance schedules (list of autos, list of locations, list of lenders, etc..)

function schedules(state={}, action) {
  let newState = {...state};
  console.log('LOG1top of reducer - is spread working');
  console.log('LOG2newState === state', newState === state);
  switch(action.type){

    case 'UPDATE_ANSWER_OBJECT':
      //receives qref and answer always, schedule and iterator if answer is part of a schedule.
      //replaces the answer object (existing or blank)
      //make sure when passing actions that you don't overwrite the answer
      console.log('LOG3inside sched reducer update answer object. initial state .certificateHolders is', state.certificateHolders[0]);
      if (action.schedule !== null) {
        if(action.qref === "shortname") {
           newState[action.schedule][action.iterator].shortname = action.answer;
           console.log('LOG4inside sched reducer update answer object. again showing initial state .certificateHolders is', state.certificateHolders[0]);
           console.log('LOG5inside sched reudes update answer object updating newState for shortname. newImmutState is', newState.certificateHolders[0]);
           return newState;
         } //don't need to build object for the shortname, which isn't part of keys or extra
        let theNewAnswer = action.answer;
        newState[action.schedule][action.iterator][action.keyInfoOrExtraInfo][action.qref].answer = theNewAnswer;
        console.log('LOG6inside sched reducer update answer object. 3rd time showing initial state .certificateHolders is', state.certificateHolders[0]);
        console.log('LOG7inside sched reudes update answer object updating newState for shortname. newImmutState is', newState.certificateHolders[0]);
        return newState;
      }
      else {
        // console.log('did not update schedules reducer because schedule was something other than null');
        return state;
      }

    break;

If relevant, this is the react component. It receives this.props rather than having it's own separate connect. I would expect this.props to reflect the pre-updated redux state and nextProps post-update, but the console logs in componentWillReceiveProps and shouldComponentUpdate all always reflect the updated props.

import React from 'react';
import ReactDOM from 'react-dom';
import { Link } from 'react-router'
import Question from './Question';
import Card from './Card';
import LineOfCoverageDescription from './LineOfCoverageDescription';
import * as Helpers from '../helpers';
import * as _ from 'lodash';
import componentHelpers from '../componentHelpers';

class TriColumns extends React.Component{
//the Link component is used instaed of normal <a> tag because it uses react-router
//received 'schedule' as props identifing what schedule is being worked on
//when want a subquestion to be a followup question (indented) pass it an extra prop that is followup="followup-question"

  constructor(){
    super();
    this.buildCardStack = this.buildCardStack.bind(this);
    this.hydrateInProcessSchedCarousel = this.hydrateInProcessSchedCarousel.bind(this);
    this.hydrateQShown = this.hydrateQShown.bind(this);
    this.firstNode = this.firstNode.bind(this);
    this.nextNode = this.nextNode.bind(this);
    this.loopShowQuestions = this.loopShowQuestions.bind(this);
    this.shortnameOrCvgDesc = this.shortnameOrCvgDesc.bind(this);
    this.showButtons = this.showButtons.bind(this);
    this.nextQTrigger = this.nextQTrigger.bind(this);
  }

  loopShowQuestions(arrayOfQuestions){
    const iterator = this.props.scheduleCarousel.schedNode;

    return(
        arrayOfQuestions.map(
          (key, i) => {
          return (
          <div key={`d${key}`} style={{'width': '100%', 'display': 'table', 'minHeight': '150px'}}>
          <Question
            {...this.props}
            qref={key}
            key={i}
            schedule={this.props.schedule}
            iterator={iterator}
          />
          <br />
          </div>
        )
      }
        )
    )
  }

  buildCardStack(){
    const thisSchedule = this.props.schedules[this.props.schedule];
    let stackers = [... thisSchedule];
    stackers.splice(this.props.scheduleCarousel.schedNode, 2); //modify stackers by removing card in the active column
    if (thisSchedule.length < 2) {
      return (<span />) //No card stack because only have 1 card
    }
    return(
        stackers.map(
          (node, i) => {
            return (
              <Card
                {...this.props}
                schedule={this.props.schedule}
                key={i}
                iterator={i}
                stacked={i}
              />
            )
          }
        )
    )
  }

  hydrateInProcessSchedCarousel(){
    // const existingInProcess = this.props.scheduleCarousel.inProcess; //first clear out any remaining questions in case not all were answered
    // console.log('rehydrating in process about to begin. first showing pre-rehydrate state of inProcess');
    // console.log(existingInProcess);
    // existingInProcess.map(
    //   (qref, i) => {
    //     this.props.removeFromSchedCarousel(qref, 'inProcess');
    //   }
    // );
    this.props.clearSchedCarousel('inProcess')
    let schedDefaultCarousel;
    if(this.props.schedule === "coverages") {
      const targetCvgNode = this.props.schedules.coverages[this.props.scheduleCarousel.schedNode];
      const coveragesList = this.props.coverages;
      schedDefaultCarousel = coveragesList[targetCvgNode.shortname].initialCarousel;
      // schedDefaultCarousel = this.props.coverages[]
    }
    else {
      schedDefaultCarousel = this.props.scheduleCarousel[this.props.schedule];
    }

    schedDefaultCarousel.map(
      (qref, i) => {
        this.props.addToSchedCarousel(qref, 'inProcess');
      }
    );
    return [... schedDefaultCarousel]
  }

  hydrateQShown(carousel){
    this.props.clearShownQuestions();
    const shownLeft = this.props.questionsShown.left;
    if (carousel == shownLeft) {
      return ""
    }
    let shouldShow;
      shouldShow = (typeof(carousel[0]) === "undefined") ? false : true;
      shouldShow ? this.props.showQuestion(carousel[0]) : "";
  }

  firstNode(){
    //call when first load the component
    let nodeSched;
    if(this.props.schedule === "coverages") {
        nodeSched = 0;  //for coverages always start at first and work to end (unlike on other schedules where start on last entered node)
    }
    else {
      nodeSched = Math.max(this.props.schedules[this.props.schedule].length-1, 0) //should be zero if an empty schedule, but if not goes to last one
    }

    this.props.setSchedNode(nodeSched);
    this.hydrateQShown(this.hydrateInProcessSchedCarousel());
  }

  nextNode(e){
    //call when it's time to go to the next question
    e.preventDefault();
    this.props.removeFromSchedCarousel(this.props.questionsShown.left[0], 'inProcess');
    this.props.unshowQuestion(this.props.questionsShown.left[0]);
    this.props.createBlankSchedNode(this.props.schedule) //create a new blank node
    let nodeSched;
    if(this.props.schedule === "coverages") {
        nodeSched = this.props.scheduleCarousel.schedNode + 1;  //for coverages always start at first then go through until end (unlike on other schedules where start on last entered node)
    }
    else {
      nodeSched = Math.max(this.props.schedules[this.props.schedule].length-1, 0) //identify newly created blank node (last in the array)
    }
    this.props.setSchedNode(nodeSched); //set nodeReference for questions to newly created blank node (last in the array)
    this.hydrateQShown(this.hydrateInProcessSchedCarousel());

  }

  nextQ(e){
    e.preventDefault();
    this.props.removeFromSchedCarousel(this.props.questionsShown.left[0], 'inProcess');
    this.props.unshowQuestion(this.props.questionsShown.left[0]);
    // this.forceUpdate();
    this.hydrateQShown(this.props.scheduleCarousel.inProcess);
  }

  shortnameOrCvgDesc(){
    if (this.props.schedule === "coverages") {
      return (
        <LineOfCoverageDescription
        {...this.props}
        lref={this.props.schedules.coverages[this.props.scheduleCarousel.schedNode].shortname}
        />
      )
    }
    const shortname = this.props.schedules[this.props.schedule][Math.max(this.props.schedules[this.props.schedule].length-1, 0)].shortname;
    return (
        <h3>{shortname}</h3>
    )
  }

  componentWillMount(){
    const initOrNot = Helpers.isEmpty(this.props.schedules[this.props.schedule]);
    initOrNot ? this.props.createBlankSchedNode(this.props.schedule) : "";
    this.firstNode();
  }

  componentWillReceiveProps(nextProps){
    console.log('inside componentWillReceiveProps in TriColumns. this.props.schedules.certificateHolders:', this.props.schedules.certificateHolders[0]);
    console.log('inside componentWillReceiveProps in TriColumns. nextProps.schedules.certificateHolders:', nextProps.schedules.certificateHolders[0]);
  }

  shouldComponentUpdate(nextProps){
    console.log('inside shouldComponentUpdate in TriColumns. this.props.schedules.certificateHolders:', this.props.schedules.certificateHolders[0]);
    console.log('inside shouldComponentUpdate in TriColumns. nextProps.schedules.certificateHolders:', nextProps.schedules.certificateHolders[0]);
    return true;
  }

  showButtons(){
    //renders the appropriate buttons based on the current state
    const showNextQ = this.props.scheduleCarousel.inProcess.length !== 1 ? true : false;
    const showNextNode = !showNextQ;
    const showFinished = !showNextQ;
    this.nextQTrigger();
    return (
      <div id="submit-or-clear">
        <fieldset>
          {showNextQ ? <button type="button" className="btn btn-info" style={{'margin': '10px'}} onClick={(e) => {this.nextQ(e)}} >Next Question</button> : ""}
          {showNextNode ? <button type="button" className="btn btn-primary" style={{'margin': '10px'}} onClick={(e) => {this.nextNode(e)}} >Next in {Helpers.camelToClassy(this.props.schedule)}</button> : ""}
          {showFinished ? <button type="button" className="btn btn-default" style={{'margin': '10px'}} onClick={(e) => {this.props.changeCenterStage('SchedCoverPage')}} >Done with {Helpers.camelToClassy(this.props.schedule)}</button> : ""}
        </fieldset>
      </div>
    )
  }

  nextQTrigger(){
    //trigger this within showbuttons and see if can trigger re-render on question answer
    //change. The problem is can't tie to onblur event directly because that's part of the question\
    //component...

    const qref = this.props.scheduleCarousel.inProcess[0];
    const iterator = this.props.scheduleCarousel.schedNode;
    const keyOrExtra = componentHelpers.determineIfKeyInfoOrExtraInfo(qref, this.props.schedule);
    const answer = this.props.schedules[this.props.schedule][iterator][keyOrExtra][qref];
    // console.log('this.props fromnextQTriggers:', this.props);
    // console.log('qref from nextQTrigger', qref);
    // console.log('iterator from nextQTrigger', iterator);
    // console.log('schedule from nextQTrigger', this.props.schedule);
    // console.log('keyOrExtra from nextQTrigger', keyOrExtra);
    // console.log('answer from nextQTrigger', answer);

  }

  render(){
    const iterator = this.props.scheduleCarousel.schedNode;

    return (
      <div id="app" className="container-fluid">
    <form>
      <div className="row-fluid">
        <div id="left-column" className="col-sm-6">
          {this.shortnameOrCvgDesc()}
          {this.loopShowQuestions(this.props.questionsShown.left)}
          {this.showButtons()}
        </div>
        <div id="active-card-column" className="col-sm-4">
          <Card
            {...this.props}
            schedule={this.props.schedule}
            key={this.props.scheduleCarousel.schedNode}
            iterator={this.props.scheduleCarousel.schedNode}
          />
        </div>
        <div id="card-stack-column" className="col-sm-2">
          {this.buildCardStack()}
        </div>
      </div>
    </form>
  </div>
    )
  }
};

export default TriColumns;

@gate5th : yes, that is _absolutely_ doing direct mutation. You're only copying the first level of nesting - you need to copy and update _every_ level of nesting. You also shouldn't copy newState like that at the _start_ of the reducer - only copy it when you actually hit a case that needs to update.

I'd encourage you to read through http://redux.js.org/docs/recipes/reducers/ImmutableUpdatePatterns.html , which shows how to do proper nested immutable updates, and discusses several common mistakes, as well as some of the articles listed at http://redux.js.org/docs/recipes/reducers/PrerequisiteConcepts.html . There's also a new FAQ section on immutability: http://redux.js.org/docs/faq/ImmutableData.html .

Thank you for the super fast response @markerikson! I had not yet seen that new FAQ on immutability and looks extensive so that will be helpful. I didn't realize that the copying had to go further down since I wasn't modifying the middle. I'll read the links in detail and see if I can determine the fix.

This is actually my first coding post: from an etiquette standpoint should I re-post with a solution once I've resolved it? Or is that unnecessary?

@gate5th : since this was a usage question, it actually should probably have been posted over on Stack Overflow, especially since this is a dead thread. You're lucky I saw the notification and answered :)

No need to re-post anything here. It's a common issue (which is why it's discussed in the docs and FAQ).

Glad you got it working now!

Was this page helpful?
0 / 5 - 0 ratings