167

I have the following state:

this.setState({ selected: { id: 1, name: 'Foobar' } });  

Then I update the state:

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

Since setState is suppose to merge I would expect it to be:

{ selected: { id: 1, name: 'Barfoo' } }; 

But instead it eats the id and the state is:

{ selected: { name: 'Barfoo' } }; 

Is this expected behavior and what's the solution to update only one property of a nested state object?

norbitrial
  • 14,716
  • 7
  • 32
  • 59
Pickels
  • 33,902
  • 26
  • 118
  • 178

13 Answers13

147

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);
}
andreypopp
  • 6,887
  • 5
  • 26
  • 26
  • 8
    Does this work reliably if you do it more than once or is there a chance react will queue the replaceState calls and the last one will win? – edoloughlin Sep 24 '14 at 21:20
  • 112
    For people that prefer using extend and that are also using babel you can do `this.setState({ selected: { ...this.state.selected, name: 'barfoo' } })` which gets translated to `this.setState({ selected: _extends({}, this.state.selected, { name: 'barfoo' }) });` – Pickels Jul 11 '15 at 20:57
  • 8
    @Pickels this is great, nicely idiomatic for React and doesn't require `underscore`/`lodash`. Spin it off into its own answer, please. – ericsoco Nov 18 '15 at 20:41
  • 15
    `this.state` [isn't necessarily up-to-date](https://facebook.github.io/react/docs/component-api.html#setstate). You can get unexpected results using the extend method. Passing an update function to `setState` is the correct solution. – Strom May 30 '16 at 16:00
  • If you are going to use underscore/lodash, you might want to look at _.merge: http://stackoverflow.com/questions/19965844/lodash-difference-between-extend-assign-and-merge – Sigfried Mar 24 '17 at 10:58
  • @Pickels, what is "_extends" in your example? Doesn't look like a JS builtin. – Edward D'Souza Apr 27 '17 at 17:35
  • 1
    @EdwardD'Souza It's the lodash library. It's commonly invoked as an underscore, same way jQuery is commonly invoked as a dollarsign. (So, it's _.extend().) – mjk May 15 '17 at 03:33
  • 1
    Lodash/underscore....where's the love for _native_ `Object.assign`? As long as you're doing a shallow copy that's all you need. – Madbreaks Jan 31 '18 at 18:05
97

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.

Edgar
  • 6,022
  • 8
  • 33
  • 66
user3874611
  • 979
  • 6
  • 3
  • 8
    update is deprecated. https://facebook.github.io/react/docs/update.html – thedanotto Oct 25 '16 at 18:49
  • 4
    @thedanotto It looks like the `React.addons.update()` method is deprecated, but the replacement library https://github.com/kolodny/immutability-helper contains an equivalent `update()` function. – Bungle Feb 01 '17 at 02:25
38

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.

Mogsdad
  • 44,709
  • 21
  • 151
  • 275
bgannonpl
  • 471
  • 4
  • 3
  • 6
    Is there an example or more docs on just how to do this? afaik, no one takes this into account. – Josef.B Jul 06 '16 at 16:31
  • @Dennis Try my answer below. – Martin Dawson Aug 26 '16 at 14:57
  • I don't see where the problem is. What you are describing would only occur if you try to access the "new" state after calling setState. That is an entirely different problem that doesn't have anything to do with the OP question. Accessing the old state before calling setState so you can construct a new state object would never be a problem, and in fact is precisely what React expects. – nextgentech Nov 15 '17 at 07:07
  • @nextgentech _"Accessing the old state before calling setState so you can construct a new state object would never be a problem"_ - you're mistaken. We're talking about _overwriting_ state based on `this.state`, which may be stale (or rather, pending queued update) when you access it. – Madbreaks Jan 31 '18 at 18:09
  • @Madbreaks No, the only time there is a problem is if you try to reference the current state _after_ already modifying it with a call to setState. Bottom line, don't call setState multiple times within a function (or use the method stated in the above answer). If you need to perform complex operations on the state then copy the state into a new object first, perform your transform, and write it all back with a single setState call. – nextgentech Jan 31 '18 at 19:59
  • @nextgentech _" If you need to perform complex operations on the state then copy the state into a new object first"_ *THAT* is the whole problem. You cannot "copy the state into a new object" using e.g. `let s = this.state`, because it may already be stale by that point. Not my opinion, documented fact. This is why `setState` is supposed be passed a function if/when you need to access current, up-to-date state. – Madbreaks Jan 31 '18 at 20:02
  • 2
    @Madbreaks Ok, yes, upon re-reading the official docs I see that it is specified that you must use the function form of setState to reliably update state based on previous state. That said, I've never come across a problem to date if not trying to call setState multiple times in the same function. Thanks for responses that got me to re-read the specs. – nextgentech Jan 31 '18 at 22:03
11

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.

Ryan Shillington
  • 23,006
  • 14
  • 93
  • 108
  • Upvote, check. Just wondering why not do it like this? `this.setState({ selected: Object.assign(this.state.selected, { name: "Barfoo" }));` – Justus Romijn Aug 07 '17 at 14:08
  • 1
    @JustusRomijn Unfortunately then you'd be modifying the current state directly. `Object.assign(a, b)` takes attributes from `b`, insert/overwrites them in `a` and then returns `a`. React people get uppity if you modify the state directly. – Ryan Shillington Aug 07 '17 at 19:28
  • Exactly. Just note that this only works for shallow copy/assign. – Madbreaks Jan 31 '18 at 18:10
  • 1
    @JustusRomijn You can do this as per [MDN assign](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Object/assign#Merging_objects_with_same_properties) in one line if you add `{}` as the first argument: `this.setState({ selected: Object.assign({}, this.state.selected, { name: "Barfoo" }) });`. This does not mutate the original state. – icc97 Apr 14 '18 at 00:35
  • @icc97 Nice! I added that to my answer. – Ryan Shillington Apr 16 '18 at 17:22
  • You say the answer doesn't violate any of the concerns, but as soon as you use `this.state.selected` on the very first line, couldn't that become out-of-date/stale? So now you've created a copy of data (fresh at the time) but by the time `this.setState()` gets processed, you're using that merged data (which then has a very stale part). For example, if `newSelected.age` was another property, say with value 1 during the name assignment, a different setState() could modify it to 2, get queued and processed, then the merged setState() sets it unintentionally back to 1 again. – thorie Jan 16 '21 at 14:28
  • @thorie This is theoretically possible. You'd get around that by putting those 3 lines of code in a `setState(() => ....)` call of its own. See more here about the updater function: https://reactjs.org/docs/react-component.html#setstate – Ryan Shillington Jan 19 '21 at 20:13
9

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"}
    }
});
Madbreaks
  • 19,094
  • 7
  • 58
  • 72
Martin Dawson
  • 7,455
  • 6
  • 49
  • 92
4

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};
});
egidiocs
  • 2,747
  • 3
  • 21
  • 20
2

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:

Mogsdad
  • 44,709
  • 21
  • 151
  • 275
Dorian
  • 2,571
  • 23
  • 33
  • Sorry for the late reply, but your Mixin example seems interesting. However, if I have 2 nested states, e.g. this.state.test.one and this.state.test.two. Is it correct that only one of these will be updated and the other one will be deleted? When I update the first one, if the second one i in the state, it will be removed... – mamruoc Dec 07 '15 at 23:28
  • hy @mamruoc, `this.state.test.two` will still be there after you update `this.state.test.one` using `setStateInDepth({test: {one: {$set: 'new-value'}}})`. I use this code in all my components in order to get around React's limited `setState` function. – Dorian Dec 09 '15 at 20:49
  • 1
    I found the solution. I initiated `this.state.test`as an Array. Changing to Objects, solved it. – mamruoc Dec 10 '15 at 22:42
2

I am using es6 classes, and I ended up with several complex objects on my top state and was trying to make my main component more modular, so i created a simple class wrapper to keep the state on the top component but allow for more local logic.

The wrapper class takes a function as its constructor that sets a property on the main component state.

export default class StateWrapper {

    constructor(setState, initialProps = []) {
        this.setState = props => {
            this.state = {...this.state, ...props}
            setState(this.state)
        }
        this.props = initialProps
    }

    render() {
        return(<div>render() not defined</div>)
    }

    component = props => {
        this.props = {...this.props, ...props}
        return this.render()
    }
}

Then for each complex property on the top state, i create one StateWrapped class. You can set the default props in the constructor here and they will be set when the class is initialised, you can refer to the local state for values and set the local state, refer to local functions, and have it passed up the chain:

class WrappedFoo extends StateWrapper {

    constructor(...props) { 
        super(...props)
        this.state = {foo: "bar"}
    }

    render = () => <div onClick={this.props.onClick||this.onClick}>{this.state.foo}</div>

    onClick = () => this.setState({foo: "baz"})


}

So then my top level component just needs the constructor to set each class to it's top level state property, a simple render, and any functions that communicate cross-component.

class TopComponent extends React.Component {

    constructor(...props) {
        super(...props)

        this.foo = new WrappedFoo(
            props => this.setState({
                fooProps: props
            }) 
        )

        this.foo2 = new WrappedFoo(
            props => this.setState({
                foo2Props: props
            }) 
        )

        this.state = {
            fooProps: this.foo.state,
            foo2Props: this.foo.state,
        }

    }

    render() {
        return(
            <div>
                <this.foo.component onClick={this.onClickFoo} />
                <this.foo2.component />
            </div>
        )
    }

    onClickFoo = () => this.foo2.setState({foo: "foo changed foo2!"})
}

Seems to work quite well for my purposes, bear in mind though you can't change the state of the properties you assign to wrapped components at the top level component as each wrapped component is tracking its own state but updating the state on the top component each time it changes.

2

Solution

Edit: This solution used to use spread syntax. The goal was make an object without any references to prevState, so that prevState wouldn't be modified. But in my usage, prevState appeared to be modified sometimes. So, for perfect cloning without side effects, we now convert prevState to JSON, and then back again. (Inspiration to use JSON came from MDN.)

Remember:

Steps

  1. Make a copy of the root-level property of state that you want to change
  2. Mutate this new object
  3. Create an update object
  4. Return the update

Steps 3 and 4 can be combined on one line.

Example

this.setState(prevState => {
    var newSelected = JSON.parse(JSON.stringify(prevState.selected)) //1
    newSelected.name = 'Barfoo'; //2
    var update = { selected: newSelected }; //3
    return update; //4
});

Simplified example:

this.setState(prevState => {
    var selected = JSON.parse(JSON.stringify(prevState.selected)) //1
    selected.name = 'Barfoo'; //2
    return { selected }; //3, 4
});

This follows the React guidelines nicely. Based on eicksl's answer to a similar question.

Tim
  • 101
  • 1
  • 8
2

ES6 solution

We set the state initially

this.setState({ selected: { id: 1, name: 'Foobar' } }); 
//this.state: { selected: { id: 1, name: 'Foobar' } }

We are changeing a property on some level of the state object:

const { selected: _selected } = this.state
const  selected = { ..._selected, name: 'Barfoo' }
this.setState({selected})
//this.state: { selected: { id: 1, name: 'Barfoo' } }
Community
  • 1
  • 1
Roman
  • 19,236
  • 15
  • 93
  • 97
0

React state doesn't perform the recursive merge in setState while expects that there won't be in-place state member updates at the same time. You either have to copy enclosed objects/arrays yourself (with array.slice or Object.assign) or use the dedicated library.

Like this one. NestedLink directly supports handling of the compound React state.

this.linkAt( 'selected' ).at( 'name' ).set( 'Barfoo' );

Also, the link to the selected or selected.name can be passed everywhere as a single prop and modified there with set.

gaperton
  • 3,566
  • 1
  • 20
  • 16
-3

have you set the initial state?

I'll use some of my own code for example:

    getInitialState: function () {
        return {
            dragPosition: {
                top  : 0,
                left : 0
            },
            editValue : "",
            dragging  : false,
            editing   : false
        };
    }

In an app I'm working on, this is how I've been setting and using state. I believe on setState you can then just edit whatever states you want individually I've been calling it like so:

    onChange: function (event) {
        event.preventDefault();
        event.stopPropagation();
        this.setState({editValue: event.target.value});
    },

Keep in mind you have to set the state within the React.createClass function that you called getInitialState

asherrard
  • 683
  • 1
  • 8
  • 11
-3

I use the tmp var to change.

changeTheme(v) {
    let tmp = this.state.tableData
    tmp.theme = v
    this.setState({
        tableData : tmp
    })
}
hkongm
  • 59
  • 3
  • 2
    Here tmp.theme = v is mutating state. So this is not the recommended way. – almoraleslopez Dec 10 '15 at 11:59
  • 1
    `let original = {a:1}; let tmp = original; tmp.a = 5; // original is now === Object {a: 5}` Don't do this, even if it hasn't given you issues yet. – Chris Jun 13 '17 at 20:56