Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

How to update nested states in React, should state be immutable?

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.

like image 414
J hung Avatar asked Feb 07 '17 22:02

J hung


People also ask

How do you update a nested object in React 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.

Why state should be immutable React?

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.

What is the proper way to update the state in React?

To update our state, we use this. setState() and pass in an object. This object will get merged with the current state.

What is the best method to update state in component?

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.


1 Answers

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
like image 98
Pineda Avatar answered Oct 02 '22 20:10

Pineda