Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

this.setState isn't merging states as I would expect

I think setState() doesn't do recursive merge.

You can use the value of the current state this.state.selected to construct a new state and then call setState() on that:

var newSelected = _.extend({}, this.state.selected);
newSelected.name = 'Barfoo';
this.setState({ selected: newSelected });

I've used function _.extend() function (from underscore.js library) here to prevent modification to the existing selected part of the state by creating a shallow copy of it.

Another solution would be to write setStateRecursively() which does recursive merge on a new state and then calls replaceState() with it:

setStateRecursively: function(stateUpdate, callback) {
  var newState = mergeStateRecursively(this.state, stateUpdate);
  this.replaceState(newState, callback);
}

Immutability helpers were recently added to React.addons, so with that, you can now do something like:

var newState = React.addons.update(this.state, {
  selected: {
    name: { $set: 'Barfoo' }
  }
});
this.setState(newState);

Immutability helpers documentation.


Since many of the answers use the current state as a basis for merging in new data, I wanted to point out that this can break. State changes are queued, and do not immediately modify a component's state object. Referencing state data before the queue has been processed will therefore give you stale data that does not reflect the pending changes you made in setState. From the docs:

setState() does not immediately mutate this.state but creates a pending state transition. Accessing this.state after calling this method can potentially return the existing value.

This means using "current" state as a reference in subsequent calls to setState is unreliable. For example:

  1. First call to setState, queuing a change to state object
  2. Second call to setState. Your state uses nested objects, so you want to perform a merge. Before calling setState, you get current state object. This object does not reflect queued changes made in first call to setState, above, because it's still the original state, which should now be considered "stale".
  3. Perform merge. Result is original "stale" state plus new data you just set, changes from initial setState call are not reflected. Your setState call queues this second change.
  4. React processes queue. First setState call is processed, updating state. Second setState call is processed, updating state. The second setState's object has now replaced the first, and since the data you had when making that call was stale, the modified stale data from this second call has clobbered the changes made in the first call, which are lost.
  5. When queue is empty, React determines whether to render etc. At this point you will render the changes made in the second setState call, and it will be as though the first setState call never happened.

If you need to use the current state (e.g. to merge data into a nested object), setState alternatively accepts a function as an argument instead of an object; the function is called after any previous updates to state, and passes the state as an argument -- so this can be used to make atomic changes guaranteed to respect previous changes.


I didn't want to install another library so here's yet another solution.

Instead of:

this.setState({ selected: { name: 'Barfoo' }});

Do this instead:

var newSelected = Object.assign({}, this.state.selected);
newSelected.name = 'Barfoo';
this.setState({ selected: newSelected });

Or, thanks to @icc97 in the comments, even more succinctly but arguably less readable:

this.setState({ selected: Object.assign({}, this.state.selected, { name: "Barfoo" }) });

Also, to be clear, this answer doesn't violate any of the concerns that @bgannonpl mentioned above.


Preserving the previous state based on @bgannonpl answer:

Lodash example:

this.setState((previousState) => _.merge({}, previousState, { selected: { name: "Barfood"} }));

To check that it's worked properly, you can use the second parameter function callback:

this.setState((previousState) => _.merge({}, previousState, { selected: { name: "Barfood"} }), () => alert(this.state.selected));

I used merge because extend discards the other properties otherwise.

React Immutability example:

import update from "react-addons-update";

this.setState((previousState) => update(previousState, {
    selected:
    { 
        name: {$set: "Barfood"}
    }
});

As of right now,

If the next state depends on the previous state, we recommend using the updater function form, instead:

according to documentation https://reactjs.org/docs/react-component.html#setstate, using:

this.setState((prevState) => {
    return {quantity: prevState.quantity + 1};
});

My solution for this kind of situation is to use, like another answer pointed out, the Immutability helpers.

Since setting the state in depth is a common situation, I've created the folowing mixin:

var SeStateInDepthMixin = {
   setStateInDepth: function(updatePath) {
       this.setState(React.addons.update(this.state, updatePath););
   }
};

This mixin is included in most of my components and I generally do not use setState directly anymore.

With this mixin, all you need to do in order to achieve the desired effect is to call the function setStateinDepth in the following way:

setStateInDepth({ selected: { name: { $set: 'Barfoo' }}})

For more information:

  • On how mixins work in React, see the official documentation
  • On the syntax of the parameter passed to setStateinDepth see the Immutability Helpers documentation.