6
votes

We are having a heated discussion on how to update nested state in React. Should the state be immutable or not? What is the best practice to update the state gracefully?

Say you have state structure that looks like this:

this.state = { 
                numberOfStudents: "3",
                gradeLevel: "5",
                students : [ 
                    { id : "1234",
                      firstName: "John",
                      lastName: "Doe",
                      email: "[email protected]"
                      phoneNumer: "12345"

                    },
                    { id : "56789",
                      firstName: "Jane",
                      lastName: "Doe",
                      email: "[email protected]"
                      phoneNumer: "56789"
                    },
                    { id : "11111",
                      firstName: "Joe",
                      lastName: "Doe",
                      email: "[email protected]"
                      phoneNumer: "11111"
                    }
                ]
            }

Then we want to update joe doe's phone number. A couple of ways we can do it:

mutate state + force update to rerender

this.state.students[2].phoneNumber = "9999999";
this.forceUpdate();

mutate state + setState with mutated state

this.state.students[2].phoneNumber = "9999999";
this.setState({
     students: this.state.students
});

Object.assign, this still mutate the state since newStudents is just a new reference to the same object this.state points to

const newStudents = Object.assign({}, this.state.students);
newStudents[2].phoneNumber = "9999999"
this.setState({
     students: newStudents
});

Update immutability helper (https://facebook.github.io/react/docs/update.html) + setState. This can get ugly very quickly if we have address.street, address.city, address.zip in each student object and want to update the street.

const newStudents = React.addons.update(this.state.students, {2: {phoneNumber: {$set:"9999999"}}});
this.setState({
     students: newStudents
})

Last line of the react doc for setState states that :

Never mutate this.state directly, as calling setState() afterwards may replace the mutation you made. Treat this.state as if it were immutable. https://facebook.github.io/react/docs/react-component.html

The docs states that we shouldn't use forceUpdate to rerender:

Normally you should try to avoid all uses of forceUpdate() and only read from this.props and this.state in render().

Why is this the case, what can happen if we mutate state and call setState afterward? Under what circumstances will setState() replace the mutation we made? This is a very confusing statement. Can someone please explain the possible complication of each of the scenario we are using above to set the state.

1

1 Answers

2
votes

You state that:

"Object.assign, this still mutate the state since newStudents is just a new reference to the same object this.state points to"

This statement is incorrect.
Object.assign mutates the state passed in to its first parameter. Since you pass in an empty object literal ({}), you are mutating the new object literal and not this.state.


Some background:

The principle of Immutable state has connections with Functional programming.

It is useful in React because it provides a way for React to know if the state has changed at all, one use case it is useful is for optimising when components should re-render

Consider the case of a complex state with nested objects. Mutating the state's values would alter the values of properties within the state but it would not change the object's reference.

this.state = {nestObject:{nestedObj:{nestObj:'yes'}}};

// mutate state
this.state.nestObject.nestedObj.nestObj= 'no';

How do we know if React should re-render the component?

  1. A deep equality check? Imagine what this would look like in a complex state, that's hundreds, even thousands of checks per state update...
  2. No need to check for changes, just force React to re-render everything with every state change...

Could there be an alternative to the latter two approaches?


The Immutable way

By creating a new object (and therefore a new reference), copying over the old state with Object.assign and mutating it, you get to manipulate the state values and change the object reference.

With the Immutable state approach we can then know if the state has changed by simply checking if the object references are equal.

An simplified example for the naysayers in the comments below:

Consider this simple example:

this this.state = { someValue: 'test'}
var newState = Object.assign({}, this.state);
console.log(newState);                  // logs: Object {someValue: "test"]  
console.log(this.state);                // logs: Object {someValue: "test"]

// logs suggest the object are equal (in property and property value at least...

console.log(this.state === this.state); // logs: true

console.log(this.state === newState);   // logs: false.  Objects are 
                                        // pass-by-reference, the values stored
                                        // stored in this.state AND newState
                                        // are references.  The strict equality
                                        // shows that these references
                                        // DON'T MATCH so we can see
                                        // that an intent to modify
                                        // state has been made