Click here to share this article on LinkedIn »

Despite that, as known by most of you, setState is asynchronous, and React is smart enough to handle multiple setState in one action:

clickHandler() {

this.setState({

a: 1

});

this.setState({

b: 2

});

} render() {

console.log('render');

return <button onClick={this.clickHandler}/>;

}

You will notice that, not only both a and b get updated by clicking the button, in the console, there is only one "render" printed out.

What if you have an object in the state, and want to update different properties of the object, does it still work the same way? Let’s see how we are going to update an object in setState . Imagine we have an object and we use a form to edit it.

this.state = {

todoList: {

day: '' // Monday, Tuesday, etc...

items: []

}

}

In the form, there is a dropdown list to choose a day from Monday to Sunday. Depending on which day you select, there will be another list of items for multi-select. The use case here is, if you choose Monday , and select a bunch of items, then you want to choose another day, say Tuesday . What happens to the list of items that you just selected for Monday ? We can’t keep that list as it becomes invalid since the day is no long Monday .

Of course we will need to do some sort of clean-up. The most straight-forward way is to empty the items list. Let’s take a look at the code (will use spread operator and some ES6 here, come on, you gotta know it)…

onDaySelect(day) {

this.setState({

todoList: {

...this.state.todoList,

day,

items: []

}

})

}

So, we are changing day and items in one setState . There is absolutely no problem here.

What if, the todoList is way more complex than this example. E.g. There are lots of checkboxes and inputs in this form. I would like to have a generic method to update the object.

Here I use RamdaJS as an example:

setObjectByPath(fieldPath, value) {

this.setState({

todoList: R.set(R.lensPath(fieldPath), value, this.state.todoList)

})

}

In this way, no matter how complicated the object is, you can easily set a value to a property, even it’s nested in objects or arrays. (Using lensPath here so that you can set something like todoList.someObject.someNestedField .

Here is how we use it in the above example:

onDaySelect(day) {

this.setObjectByPath(['day'], day);

} onItemsSelect(items) {

this.setObjectByPath(['items'], items);

} onSomeNestedFieldChange(value) {

this.setObjectByPath(['objectName', 'fieldName'], value);

}

Elegant!

So, let’s apply to the above example in onDaySelect .

onDaySelect(day) {

this.setObjectByPath(['day'], day); // 1

this.setObjectByPath(['items'], []); // 2

}

You will soon notice that, it somehow does not work properly.

At first, I thought, something was wrong with React, because it didn’t update both properties for me. In fact, the first line never works. Why?

I’ll skip the boring explanation. In the setObjectByPath function, it always takes this.state.todoList as the origin. As setState is async, when two setObjectByPath are called, the state hasn’t been changed at all. Imagine the todoList object is like this before the update:

{day: 'Monday', items: [1,2,3]}

Here is what actually happens when we call the onDaySelect function:

{day: 'Monday', items: [1,2,3]} -> {day: 'Tuesday', items: [1,2,3]}

{day: 'Monday', items: [1,2,3]} -> {day: 'Monday', items: []}

The problem is not on React, it’s on the function I wrote! Perhaps…

It means that, you can’t use that beautiful generic setObjectByPath method if you need to change more than 1 property at a time in setState . How lame it is?! Same thing happens to Redux .

Unlike Redux , React is smart that you don’t need to always put the final outcome of the new state in the setState function. You can expect property b remains while you only put a in setState . While in Redux , if you only return a in the new state, don’t expect you can find b anymore. That means, you need to take care of the entire state object in Redux . That also means, you need to put all the specific logic in one place and can’t delegate to multiple handlers.

It ends up with something like this in the setObjectPath :

setObjectPath(fieldPath, value) {

if (fieldPath[0] === 'day') {

this.setState({

R.compose(R.set(), R.set()) // sets day and items together

})

} else if // more other cases...

}

Although, this approach is still better than having non-generic set functions everywhere in each inputHandler .

So, what is the solution for the problem?

…

Sorry, I don’t have one for you.

The only thing I can think of is, to flatten the object and spread it to the state, like what you do to your toast with peanut butter:

this.state = {

day: '',

items: [],

someField: '',

someNestedField: ''

// ...

}

// no more todoList object

I can’t say if this approach is not elegant, or ugly, but I really don’t like it. One of the reason is, you need to re-pack all the fields into one object at the end. It is not intuitive and adds extra complexity.

Open for discussion~