2

For the following code block:

const items = [
  { id: 1, name: 'one' },
  { id: 2, name: 'two' },
];

const changes = {
  name: 'hello'
}

items.forEach((item, i) => {
  item = {
    ...item,
    ...changes
  }
})

console.log(items) // items NOT reassigned with changes

items.forEach((item, i) => {
  items[i] = {
    ...item,
    ...changes
  }
});

console.log(items) // items reassigned with changes

Why does reassigning the values right on the element iteration not change the objects in the array?

  item = {
    ...item,
    ...changes
  }

but changing it by accessing it with the index does change the objects in the array?

  items2[i] = {
    ...item,
    ...changes
  }

And what is the best way to update objects in an array? Is items2[i] ideal?

halfer
  • 19,824
  • 17
  • 99
  • 186
cup_of
  • 6,397
  • 9
  • 47
  • 94
  • 1
    That is by design. Changing the array which you are iterating through can cause bugs that are difficult to find and fix. For your purposes, it's probably better to use array.map(). – Emil Karlsson Nov 23 '21 at 08:13

4 Answers4

4

Say no to param reassign!

This is a sort of a fundamental understanding of higher level languages like JavaScript.

Function parameters are temporary containers of a given value.

Hence any "reassigning" will not change the original value.

For example look at the example below.

let importantObject = {
  hello: "world"
}

// We are just reassigning the function parameter
function tryUpdateObjectByParamReassign(parameter) {
  parameter = {
    ...parameter,
    updated: "object"
  }
}


tryUpdateObjectByParamReassign(importantObject)
console.log("When tryUpdateObjectByParamReassign the object is not updated");
console.log(importantObject);

As you can see when you re-assign a parameter the original value will not be touched. There is even a nice Lint rule since this is a heavily bug prone area.

Mutation will work here, but ....

However if you "mutate" the variable this will work.

let importantObject = {
  hello: "world"
}

// When we mutate the returned object since we are mutating the object the updates will be shown
function tryUpdateObjectByObjectMutation(parameter) {
  parameter["updated"] = "object"
}


tryUpdateObjectByObjectMutation(importantObject)
console.log("When tryUpdateObjectByObjectMutation the object is updated");
console.log(importantObject);

So coming back to your code snippet. In a foreach loop what happens is a "function call" per each array item where the array item is passed in as a parameter. So similar to above what will work here is as mutation.

const items = [
  { id: 1, name: 'one' },
  { id: 2, name: 'two' },
];

const changes = {
  name: 'hello'
}

items.forEach((item, i) => {
  // Object assign just copies an object into another object
  Object.assign(item, changes);
})

console.log(items) 

But, it's better to avoid mutation!

It's better not mutate since this can lead to even more bugs. A better approach would be to use map and get a brand new collection of objects.

const items = [{
    id: 1,
    name: 'one'
  },
  {
    id: 2,
    name: 'two'
  },
];

const changes = {
  name: 'hello'
}

const updatedItems = items.map((item, i) => {
  return {
    ...item,
    ...changes
  }
})

console.log({
  items
})
console.log({
  updatedItems
})
Dehan
  • 4,818
  • 1
  • 27
  • 38
2

As the MDN page for forEach says:

forEach() executes the callbackFn function once for each array element; unlike map() or reduce() it always returns the value undefined and is not chainable. The typical use case is to execute side effects at the end of a chain.

Have a look here: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Array/forEach

This means that although you did create new object for item, it was not returned as a value for that index of array. Unlike your second example, the first one is not changing original array, but just creates new objects and returns undefined. This is why your array is not modified.

Lazar Nikolic
  • 4,261
  • 1
  • 22
  • 46
  • 1
    This has nothing to with how forEach works. `item = { some object }` just reassigns the local variable `item` with a new value and doesn't affect the array. – adiga Nov 23 '21 at 08:13
  • I believe this is what I wrote. It looks to me that you just rephrase what I wrote. – Lazar Nikolic Nov 23 '21 at 08:15
  • Whether forEach returns undefined has nothing do with the issue. `for(let item of items) item = { ...changes }` will also have the same effect. – adiga Nov 23 '21 at 08:16
2

I'd go with a classic Object.assign for this:

const items = [
  { id: 1, name: 'one' },
  { id: 2, name: 'two' },
];

const changes = {
  name: 'hello'
}

items.forEach( (item) => Object.assign(item,changes) )

console.log(items) 

Properties in the target object are overwritten by properties in the sources if they have the same key. Later sources' properties overwrite earlier ones. https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Object/assign

malarres
  • 2,941
  • 1
  • 21
  • 35
0

The other approach you can take is to use map and create a new array based on the original data and the changes:

const items = [
  { id: 1, name: 'one' },
  { id: 2, name: 'two' },
];

const changes = {
  name: 'hello'
}

const newItems = items.map((item) => {
  ...item,
  ...changes
})

console.log(newItems);

But if you need to modify the original array, it's either accessing the elements by index, or Object.assign. Attempting to assign the value directly using the = operator doesn't work because the item argument is passed to the callback by value not by reference - you're not updating the object the array is pointing at.

Krzysztof Woliński
  • 328
  • 1
  • 2
  • 12