'React: Updating state when state is an array of objects

I have an array of objects in state:

this.state = {
  items: [
    {id: 1, someattr: "a string", anotherattr: ""},
    {id: 2, someattr: "another string", anotherattr: ""},
    {id: 3, someattr: "a string", anotherattr: ""},
  ]
}

I need to be able to search the items array based on the id property and then update the objects attributes.

I can get the object by filtering or finding on the array using the id param.

What I'm having trouble with is then updating the array and then subsequently updating state without mutation.

//make sure we're not mutating state directly by using object assign
const items = Object.assign({}, this.state.items);
const match = items.find((item) => item.id === id);

At this point I have a matching object and can update it's properties using object spread:

const matchUpdated = { ...match, someattr: 'a new value'};

My question is how do i then update the state with matchUpdated so that it overwrites the object returned by the initial find operation?



Solution 1:[1]

Your update function would look like this

updateItem(id, itemAttributes) {
  var index = this.state.items.findIndex(x=> x.id === id);
  if (index === -1)
    // handle error
  else
    this.setState({
      items: [
         ...this.state.items.slice(0,index),
         Object.assign({}, this.state.items[index], itemAttributes),
         ...this.state.items.slice(index+1)
      ]
    });
}

And you use it like this

this.updateItem(2, {someattr: 'a new value'});

Gross right?


You're going to have a big headache in general if you continue to build a complex application in this manner. I would recommend you look into redux or some other Flux implementation that is better suited for solving these problems.

Redux uses a concept of state reducers which each work on a specific slice of the state of your application. That way you don't have to manually dig through your entire state each time you want to affect a deep change.

The creator of Redux, Dan Abramov, has made two video courses available online for free. Dan is an excellent teacher and I felt comfortable with the Redux pattern after spending just one afternoon with it.

Solution 2:[2]

If you wish to use a function, this is how I would do it...

const[array,setArray]= useState([
    {id: 1, value: "aws", othervalue: "was"},
    {id: 2, value: "goo", othervalue: "nano"},
    {id: 3, value: "micro", othervalue: "marcro"},
])

const updateItem =(id, whichvalue, newvalue)=> {
    let index = array.findIndex(x=> x.id === id); 
/*this line is only neccessay if your element's id 
isn't its postion/index in the array or related to it.
In the case that it is, use the id as the index, or run the function
(binary/hash) that relates the id to that position/index to find the index.
*/
    if (index !== -1){
        let temporaryarray = array.slice();
        temporaryarray[index][whichvalue] = newvalue;
        setArray(temporaryarray);
    }
    else {
        console.log('no match');
    }
}
    /* --longer version--
    var index = array.findIndex(x=> x.id === id);
    let g = array[index]
    g[whichvalue] = newvalue
    if (index === -1){
        console.log('no match')
    }
    else {
        setArray(
            [
            ...array.slice(0,index),
            g,
            ...array.slice(index+1)
            ]
        );
    }
    */
    
//how to use the function    
onPress={()=>updateItem(2,'value','John Lemon')}
onPress={()=>updateItem(1,'othervalue','Stringo Stra')}

The first input of the function is the id of the item.

The second input of the function is the attribute that you wish to change.

The third input of the function is the new value for that attribute.

Solution 3:[3]

If you were using functional components and the useState hook, you could easily use map, as long as you don't mind substituting the entire object

const [items, setItems] = useState ([
    {id: 1, someattr: "a string", anotherattr: ""},
    {id: 2, someattr: "another string", anotherattr: ""},
    {id: 3, someattr: "a string", anotherattr: ""},
])

setItems (
    items.map((item) => {
        return item.id === updatedItem.id? updatedItem: item;
    })
); 

Solution 4:[4]

Adding to answer of Mulan , you could use Object spread which much cleaner and more readable than Object.assign()

updateItem(id, itemAttributes) {
  var index = this.state.items.findIndex(x=> x.id === id);
  if (index === -1)
    // handle error
  else
    this.setState({
      items: [
         ...this.state.items.slice(0,index),
        { ...this.state.items[index], itemAttributes },  
         ...this.state.items.slice(index+1)
      ]
    });
}

And you use it like this

this.updateItem(2, {someattr: 'a new value'});

Solution 5:[5]

There is also a one-line solution:

this.setState({items: this.state.items.map(x => x.id === someId ? {...x, attr:'val'} : x)});

The key is to return a new array instead of mutating the state.

Update 2022-04-27

As @douglas-gaskell pointed out, this isn’t the most performant method given that we rebuild array one item at a time. For very large arrays, where making a copy every item one-by-one would visibly strain performance, the better option is to use solution proposed by @Mulan.

Contrary to .map(), .slice() method returns a shallow copy of a whole portion of an array and is therefore more performant (see benchmark here).

Solution 6:[6]

You can loop through the values of the state array object and replace the state. In the loop you can decide on which record you want to apply the new value if not all.

this.state = {
  items: [
    {id: 1, someattr: "a string", anotherattr: ""},
    {id: 2, someattr: "another string", anotherattr: ""},
    {id: 3, someattr: "a string", anotherattr: ""},
  ]
}

//Answer

const newState = Object.assign({}, this.state);
   
newState.items.forEach(element => {

  element.someattr= "I like it a lot";

});

this.setState(newState);

Solution 7:[7]

in function component like this you have to add it to the function

const [item,setitem]=usestate([{id_1:null},{id_2:null}])
()=> setitems((prev) => { return { ...prev, id_1: "my change value"} }) 

and use it like this

  console.log(items.id_1 == "my change value")

Sources

This article follows the attribution requirements of Stack Overflow and is licensed under CC BY-SA 3.0.

Source: Stack Overflow

Solution Source
Solution 1
Solution 2
Solution 3 sagg1295
Solution 4 Hussam Khatib
Solution 5
Solution 6 Alfred Alfizo Mosima
Solution 7