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.
To update nested properties in a state object in React: Pass a function to setState to get access to the current state object. Use the spread syntax (...) to create a shallow copy of the object and the nested properties. Override the properties you need to update.
In React, using an Immutable state enables quick and cheap comparison of the state tree before and after a change. As a result, each component decides whether to re-rendered or not before performing any costly DOM operations.
To update our state, we use this. setState() and pass in an object. This object will get merged with the current state.
setState method allows to change of the state of the component directly using JavaScript object where keys are the name of the state and values are the updated value of that state. Often we update the state of the component based on its previous state.
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
.
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?
Could there be an alternative to the latter two approaches?
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.
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
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With