47

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?

6
  • Why not just use object.assign again? Commented Jun 6, 2016 at 16:59
  • Could you give an example as I'm not sure how that'd work ? Commented Jun 6, 2016 at 17:00
  • 1
    const newState = Object.assign(this.state.items, matched) this.setState({items: newState}) ... if I'm understanding correctly Commented Jun 6, 2016 at 17:02
  • 1
    Did you look at facebook.github.io/react/docs/update.html ? Commented Jun 6, 2016 at 17:15
  • Why couldn't you just use this.state = { items: { 1: { someattr: ... }, 2: { ... }, ... } };? this.state.items[id] = { ...this.state.items[id], someattr: 'a new value' }. Commented Jun 6, 2016 at 17:48

7 Answers 7

62

2025

Here's an update showing a live code demo so you can verify the results in your own browser -

function App(props) {
  const [state, setState] = React.useState(props.initialState)
  function updateObj(index, key, value) {
    setState(s => [
      ...s.slice(0, index),
      { ...s[index], [key]: value },
      ...s.slice(index + 1),
    ])
  }
  return <div>
    {state.map((o, index) =>
      <div key={o.id}>
        <input value={o.foo} onChange={e => updateObj(index, "foo", e.target.value)} />
        <input value={o.bar} onChange={e => updateObj(index, "bar", e.target.value)} />
        <input value={o.qux} onChange={e => updateObj(index, "qux", e.target.value)} />
      </div>
    )}
    <pre>{JSON.stringify(state, null, 2)}</pre>
  </div>
}

ReactDOM.createRoot(document.querySelector("#app")).render(
  <App
    initialState={[
      { id: 1, foo: "a", bar: "b", qux: "c" },
      { id: 2, foo: "j", bar: "k", qux: "l" },
      { id: 3, foo: "x", bar: "y", qux: "z" },
    ]}
  />
)
<script crossorigin src="https://unpkg.com/react@18/umd/react.development.js"></script>
<script crossorigin src="https://unpkg.com/react-dom@18/umd/react-dom.development.js"></script>
<div id="app"></div>

2020

Things have changed a lot since I wrote this, but leaving the original for posterity.

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.

Sign up to request clarification or add additional context in comments.

2 Comments

this is a really bad approach if if the array length is somewhere around 50-80 this means you are using .find() everytime the user interacts with the .name textbox.
find is not an essential part of the code. In most cases, the index to update will be known ahead of time. You can use a Map but note that key-value pairs are stored in insertion order.
10

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

Comments

9

If you wish to use a function, this is how I would do it. The function arguments are the ID of the item, the attribute that you wish to change, and the new value for that attribute.

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 necessary if your element's ID 
     isn't its position/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')
}

1 Comment

Is temporaryarray[index][whichvalue] = newvalue; a valid way to update state? Slicing the array creates a new array reference, but the references in the array still point to the original objects. So on that line you're directly updating an object that is in state, right?
2

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

Comments

1

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.

As @douglas-gaskell rightly pointed out, this is an O(n) operation, while @Mulan's answer is O(1). But you know how it is - algorithmic complexity matters when the n gets large. When number of items is large, I use @Mulan's answer, but when the number of items is small, I use this solution.

4 Comments

And when iterating an entire array of items when a value in one of those items is changed becomes a performance problem? This seems far less than ideal.
Good point @Douglas Gaskell. This solution works great with small arrays, for larger I use destructuring assignment with slicing similar to presented by Mulan.
Unfortunately array.slice is still an O(n) operation unless you have an array of only numbers, in which case it's vectorized :/ On low-end devices large array copies are slow. But that aside, it is quite verbose & it seems.... excessive. Strict immutability is more of a hindrance than a boon here.
Agree, but this is still a valid solution to most of use cases. Added a explanation so that everyone can make their own decision.
0

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

Comments

-3

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")

Comments

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.