Home » Reactjs » React: How do I update state.item[1] on setState? (with JSFiddle)

React: How do I update state.item[1] on setState? (with JSFiddle)

Posted by: admin November 30, 2017 Leave a comment

Questions:

I’m creating an app where the user can design his own form. E.g. specify name of the field and details of which other columns that should be included.

The component is available as a JSFiddle here.

My initial state looks like this:

   77   var DynamicForm = React.createClass({
   78     getInitialState: function() {
   79       var items = {};
   80       items[1] = { name: 'field 1', populate_at: 'web_start',
   81                    same_as: 'customer_name',
   82                    autocomplete_from: 'customer_name', title: '' };
   83       items[2] = { name: 'field 2', populate_at: 'web_end',
   84                    same_as: 'user_name', 
                         autocomplete_from: 'user_name', title: '' };
   85
   86       return { items };
   87     },


  108     render: function() {
  109       var _this = this;
  110       return (
  111         <div>
  112           { Object.keys(this.state.items).map(function (key) {
  113             var item = _this.state.items[key];
  114             return (
  115               <div>
  120                 <PopulateAtCheckboxes this={this}
  121                   checked={item.populate_at} id={key} 
                        populate_at={data.populate_at} />
  129               </div>
  130               );
  131           }, this)}
  132           <button onClick={this.newFieldEntry}>Create a new field</button>
  133           <button onClick={this.saveAndContinue}>Save and Continue</button>
  134         </div>
  135       );
  136     }

I want to update the state when the user changes any of the values, but I’m having a hard time to target the correct object:

   19   var PopulateAtCheckboxes = React.createClass({
   20     handleChange: function (e) {
   21        item = this.state.items[1];
   22        item.name = 'newName';
   23        items[1] = item;
   24
   25        this.setState({items: items});
   26     },
   27
   28     render: function() {
   29       var populateAtCheckbox = this.props.populate_at.map(function(value) {
   30         return (
   31           <label for={value}>
   32             <input type="radio" name={'populate_at'+this.props.id} value={value}
   33               onChange={this.handleChange} checked={this.props.checked == value}
   34               ref="populate-at"/>
   35             {value}
   36           </label>
   37         );
   38       }, this);
   39       return (
   40         <div className="populate-at-checkboxes">
   41           {populateAtCheckbox}
   42         </div>
   43       );
   44     }
   45   });

How should I craft this.setState to get it to update items[1].name ?

Answers:

With ES6:

handleChange(e) {
    const items = this.state.items;
    items[1].role = e.target.value;

    // update state
    this.setState({
        items,
    });
},

Or if you would export your class directly:

handleChange = (e) => {
    const items = this.state.items;
    items[1].role = e.target.value;

    // update state
    this.setState({
        items,
    });
};

Update!

As pointed out by a lot of better developers in the commentes: mutating the state is wrong. It’s not needed to use “setState” because your modifying a child value. The only reason this works is because “setState” does a “forceUpdate” which re-renders the view.

So it would be better to do:

handleChange = (e) => {
    const items = this.state.items;
    items[1].role = e.target.value;

    // re-render
    this.forceUpdate();
};

This way it still changes your value and re-renders the view with the new data.

Questions:
Answers:

You could use the update immutability helper for this:

this.setState({
  items: update(this.state.items, {1: {name: {$set: 'updated field name'}}})
})

Or if you don’t care about being able to detect changes to this item in a shouldComponentUpdate() lifecycle method using ===, you could edit the state directly and force the component to re-render – this is effectively the same as @limelights’ answer, as it’s pulling an object out of state and editing it.

this.state.items[1].name = 'updated field name'
this.forceUpdate()

Post-edit addition:

Check out the Simple Component Communication lesson from react-training for an example of how to pass a callback function from a state-holding parent to a child component which needs to trigger a state change.

Questions:
Answers:

First get the item you want, change what you want on that object and set it back on the state.
The way you’re using state by only passing an object in getInitialState would be way easier if you’d use a keyed object.

handleChange: function (e) {
   item = this.state.items[1];
   item.name = 'newName';
   items[1] = item;

   this.setState({items: items});
}

Questions:
Answers:

Don’t mutate the state in place. It can cause unexpected results. I have learned my lesson! Always work with a copy/clone, Object.assign() is a good one:

item = Object.assign({}, this.state.items[1], {name: 'newName'});
items[1] = item;
this.setState({items: items});

https://developer.mozilla.org/en/docs/Web/JavaScript/Reference/Global_Objects/Object/assign

Questions:
Answers:

EDIT:

For deep clone:

const groups = JSON.parse(JSON.stringify(this.state.groups[index]))

a real world example with ES6:

handleChange(index, e) {
   const groups = [...this.state.groups];
   groups[index].name = e.target.value;
   this.setState({ groups });
}

Questions:
Answers:

As already has been stated, to modify deeply nested objects/variables in the React’s state, typically three methods are used: vanilla JS Object.assign, immutability-helper and cloneDeep from Lodash.

As I pointed in the comments to some answers whose authors propose a direct mutation of state, just don’t do that. This is a ubiquitous React anti-pattern, which will inevitably lead to unwanted consequences. Learn the right way.

Let’s compare all the aforementioned methods.

Given this state structure:

state = {
    foo: {
        bar: 'initial value'
    }
};

Change of the innermost bar can be done like this using vanilla JavaScript (Object.assign):

(...essential imports)
class App extends Component {

    state = {
        foo: {
            bar: 'initial value'
        }
    };

    componentDidMount() {

        console.log(this.state.foo.bar); // initial value

        const foo = Object.assign({}, this.state.foo, {bar: 'further value'});

        console.log(this.state.foo.bar); // initial value

        this.setState({foo}, () => {
            console.log(this.state.foo.bar); // further value
        });
    }
    (...rest of code)

Keep in mind, that Object.assign will not perform a deep cloning, since it only copies property values, and that’s why what it does called a shallow copying (see comments). It will work, if you have a relatively simple one-level deep state structure with innermost members holding values of primitive type. Like the bar in this example, holding string.

If you have deeper objects, which you should update, don’t use Object.assign. You risk mutating state directly.

Now, the Lodash’s cloneDeep:

(...essential imports)
import {cloneDeep} from 'lodash';

class App extends Component {

    state = {
        foo: {
            bar: 'initial value'
        }
    };

    componentDidMount() {

        console.log(this.state.foo.bar); // initial value

        const foo = cloneDeep(this.state.foo);

        foo.bar = 'further value';   

        console.log(this.state.foo.bar); // initial value

        this.setState({foo}, () => {
            console.log(this.state.foo.bar); // further value
        });
    }
    (...rest of code)

Basically, Lodash’s cloneDeep performs a deep cloning, so it is a robust option if you have a fairly complex state with multi-level objects or arrays included.

Finally, this is how it’s done with immutability-helper:

(...essential imports)
import update from 'immutability-helper';

class App extends Component {

    state = {
        foo: {
            bar: 'initial value'
        }
    };

    componentDidMount() {

        console.log(this.state.foo.bar); // initial value     

        const foo = update(this.state.foo, {bar: {$set: 'further value'}});    

        console.log(this.state.foo.bar); // initial value

        this.setState({foo}, () => {
            console.log(this.state.foo.bar); // further value
        });
    }    
    (...rest of code)

There are a lot of commands available in the immutability-helper in addition to $set: $push, $splice, $merge etc.

Notice, that this.setState() only modifies the first-level properties of the state object (foo property in this case), not the deeply nested (foo.bar). If it behaved other way, this question wouldn’t exist.

And by the way, this.setState({foo}) is just a shorthand for this.setState({foo: foo}). And () => {console.log(this.state.foo.bar)} after the {foo} is a callback which gets executed immediately after setState have set the state. Convenient, if you need to do some things after it did its job.

Now when all three major methods are covered, let’s compare their speed. I created a jsperf for this. In this test a huge state with 10000 properties with random values gets manipulated by all three aforementioned methods.

Here are the results for my Chrome 61.0.3163 on Windows 10:

+---------------------+---------+-----------+-------------+
|        Test         | Ops/sec | Precision | Explanation |
+---------------------+---------+-----------+-------------+
| Object.assign       |     272 | ±0.62%    | 56% slower  |
| cloneDeep           |     618 | ±0.69%    | fastest     |
| immutability-helper |     271 | ±0.33%    | 56% slower  |
+---------------------+---------+-----------+-------------+

Note that in Edge on the same machine Lodash performed worse than two other methods. And in Firefox sometimes cloneDeep is a winner, but at other times it is Object.assign.

Here is the link to the jsperf.com test.

If you don’t want or can use external dependencies, and have a simple state structure, stick to Object.assign. If you manipulate a huge and/or complex state, Lodash’s cloneDeep is a wise choice. If you need advanced capabilities, i.e. if your state structure is complex and you need to perform all kinds of operations on it, try immutability-helper, it’s a very advanced tool which can be used for state manipulation.

Questions:
Answers:

Use the event on handleChange to figure out the element that has changed and then update it. For that you might need to change some property to identify it and update it.

See fiddle https://jsfiddle.net/69z2wepo/6164/

Questions:
Answers:

Try this it will definetly work,other case i tried but didn’t work

import _ from 'lodash';

this.state.var_name  = _.assign(this.state.var_name, {
   obj_prop: 'changed_value',
});

Questions:
Answers:

How about creating another component(for object that needs to go into the array) and pass the following as props?

  1. component index – index will be used to create/update in array.
  2. set function – This function put data into the array based on the component index.

<SubObjectForm setData={this.setSubObjectData} objectIndex={index}/>

Here {index} can be passed in based on position where this SubObjectForm is used.

and setSubObjectData can be something like this.

 setSubObjectData: function(index, data){
      var arrayFromParentObject= <retrieve from props or state>;
      var objectInArray= arrayFromParentObject.array[index];
      arrayFromParentObject.array[index] = Object.assign(objectInArray, data);
 }

In SubObjectForm, this.props.setData can be called on data change as given below.

<input type="text" name="name" onChange={(e) => this.props.setData(this.props.objectIndex,{name: e.target.value})}/>

Questions:
Answers:

I would move the function handle change and add an index parameter

handleChange: function (index) {
    var items = this.state.items;
    items[index].name = 'newName';
    this.setState({items: items});
},

to the Dynamic form component and pass it to the PopulateAtCheckboxes component as a prop. As you loop over your items you can include an additional counter (called index in the code below) to be passed along to the handle change as shown below

{ Object.keys(this.state.items).map(function (key, index) {
var item = _this.state.items[key];
var boundHandleChange = _this.handleChange.bind(_this, index);
  return (
    <div>
        <PopulateAtCheckboxes this={this}
            checked={item.populate_at} id={key} 
            handleChange={boundHandleChange}
            populate_at={data.populate_at} />
    </div>
);
}, this)}

Finally you can call your change listener as shown below here

<input type="radio" name={'populate_at'+this.props.id} value={value} onChange={this.props.handleChange} checked={this.props.checked == value} ref="populate-at"/>

Questions:
Answers:

If you need to change only part of the array,
You’ve a react component with state set to

state = {items: [{name: 'red-one', value: 100}, {name: 'green-one', value: 999}]}

It’s best to update the red-one in the array as follows

    const newItems = this.state.items
                       .filter((item) => item.name === 'red-one')
                       .map(item => item.value = 666)
    this.setState(newItems)

Questions:
Answers:

Mutation free:

// given a state
state = {items: [{name: 'Fred', value: 1}, {name: 'Wilma', value: 2}]}

// This will work without mutation as it clones the modified item in the map:
this.state.items
   .map(item => item.name === 'Fred' ? {...item, ...{value: 3}} : item)

this.setState(newItems)