98

If you have an array as part of your state, and that array contains objects, whats an easy way to update the state with a change to one of those objects?

Example, modified from the tutorial on react:

var CommentBox = React.createClass({
  getInitialState: function() {
    return {data: [
      { id: 1, author: "john", text: "foo" },
      { id: 2, author: "bob", text: "bar" }
    ]};
  },
  handleCommentEdit: function(id, text) {
    var existingComment = this.state.data.filter({ function(c) { c.id == id; }).first();
    var updatedComments = ??; // not sure how to do this  

    this.setState({data: updatedComments});
  }
}
Patrick Roberts
  • 44,815
  • 8
  • 87
  • 134
Alex Black
  • 13,334
  • 15
  • 75
  • 97
  • Could you describe what are you trying to do? – daniula Jan 24 '15 at 03:22
  • 1
    If you are looking at editing a comment, look at the following link -> https://github.com/tastejs/todomvc/blob/gh-pages/examples/react-backbone/js/todoItem.jsx. Essentially the tutorial you listed is just a simplified version of the ToDo app. – TYRONEMICHAEL Jan 24 '15 at 04:04
  • @daniula I'm looking to change the text of one of the comments. I'll add a bit more detail above. – Alex Black Jan 24 '15 at 07:03
  • Possible duplicate of [Correct modification of state arrays in ReactJS](https://stackoverflow.com/questions/26253351/correct-modification-of-state-arrays-in-reactjs) – sleske Dec 15 '17 at 07:58

4 Answers4

144

I quite like doing this with Object.assign rather than the immutability helpers.

handleCommentEdit: function(id, text) {
    this.setState({
      data: this.state.data.map(el => (el.id === id ? Object.assign({}, el, { text }) : el))
    });
}

I just think this is much more succinct than splice and doesn't require knowing an index or explicitly handling the not found case.

If you are feeling all ES2018, you can also do this with spread instead of Object.assign

this.setState({
  data: this.state.data.map(el => (el.id === id ? {...el, text} : el))
});
Not loved
  • 32,309
  • 21
  • 118
  • 187
  • 2
    Great answer, thanks! Since _you shouldn't access state in setState_, this is a safer version of it: `this.setState(prevState => ({ data: prevState.data.map(el => (el.id === id ? { ...el, text } : el)) }))` – nevermind777 Oct 30 '19 at 10:12
  • 1
    @Notloved Why is it just ({}, el, { text }) or {...el, text} in the two ways you mentioned and not ({}, el, { text: text }) or {...el, text: text}. How does it know that it has to update value of 'text' key and not the other ones? Or if the 'text' word in your answer refers to the key, where are we passing the value 'text'? I guess I am missing some fundamental understanding of how it works – user3884753 Mar 16 '21 at 15:53
  • 1
    @user3884753 `{text}` is shorthand for `{text: text}`. As `text` is defined as a variable, you can use it as both the name and value in the object without needing to define it twice. This came with ES6, and is sometimes referred to as "[shorthand property names](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Operators/Object_initializer#new_notations_in_ecmascript_2015)" – Nick Parsons Dec 08 '21 at 04:05
  • This answer can be improved by explaining what is going on in the code. – ViktorMS May 09 '22 at 09:40
53

While updating state the key part is to treat it as if it is immutable. Any solution would work fine if you can guarantee it.

Here is my solution using immutability-helper:

jsFiddle:

  var update = require('immutability-helper');

  handleCommentEdit: function(id, text) {
    var data = this.state.data;
    var commentIndex = data.findIndex(function(c) { 
        return c.id == id; 
    });

    var updatedComment = update(data[commentIndex], {text: {$set: text}}); 
    
    var newData = update(data, {
        $splice: [[commentIndex, 1, updatedComment]]
    });
    this.setState({data: newData});
  },

Following questions about state arrays may also help:

Glorfindel
  • 20,880
  • 13
  • 75
  • 99
nilgun
  • 10,160
  • 3
  • 44
  • 55
  • 5
    I ended up using map to update the array. var newData = this.state.data.map(function(c) { return c.id == id ? React.addons.update(c, update) : c; }); – Alex Black Jan 26 '15 at 04:14
24

Trying to clean up/ explain better how to do this AND what's going on.

  • First, find the index of the element you're replacing in the state array.
  • Second, update the element at that index
  • Third, call setState with the new collection
import update from 'immutability-helper';

// this.state = { employees: [{id: 1, name: 'Obama'}, {id: 2, name: 'Trump'}] } 

updateEmployee(employee) {
    const index = this.state.employees.findIndex((emp) => emp.id === employee.id);
    const updatedEmployees = update(this.state.employees, {$splice: [[index, 1, employee]]});  // array.splice(start, deleteCount, item1)
    this.setState({employees: updatedEmployees});
}

Edit: there's a much better way to do this w/o a 3rd party library

const index = this.state.employees.findIndex(emp => emp.id === employee.id);
employees = [...this.state.employees]; // important to create a copy, otherwise you'll modify state outside of setState call
employees[index] = employee;
this.setState({employees});
Shawn Mclean
  • 55,505
  • 94
  • 274
  • 404
daino3
  • 4,034
  • 33
  • 44
14

You can do this with multiple way, I am going to show you that I mostly used. When I am working with arrays in react usually I pass a custom attribute with current index value, in the example below I have passed data-index attribute, data- is html 5 convention.

Ex:

//handleChange method.
handleChange(e){
  const {name, value} = e,
        index = e.target.getAttribute('data-index'), //custom attribute value
        updatedObj = Object.assign({}, this.state.arr[i],{[name]: value});
      
  //update state value.
  this.setState({
    arr: [
      ...this.state.arr.slice(0, index),
      updatedObj,
      ...this.state.arr.slice(index + 1)
    ]
  })
  }
Umair Ahmed
  • 6,967
  • 3
  • 29
  • 33