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

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?

Answers:

Answer

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);
}
Answer

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.

Answer

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.

Answer

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.

Answer

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"}
    }
});
Answer

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:

Answer

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.

Answer

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};
});
Answer

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' } }
Answer

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.

Answer

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

Answer

I use the tmp var to change.

changeTheme(v) {
    let tmp = this.state.tableData
    tmp.theme = v
    this.setState({
        tableData : tmp
    })
}
Answer

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.

Tags

Recent Questions

Top Questions

Home Tags Terms of Service Privacy Policy DMCA Contact Us Javascript

©2020 All rights reserved.