Immutable operations on arrays

September 17, 2022JavaScript

5 min read

It's very helpful to know how to perform operations on arrays in JavaScript in an immutable way. Operations such as adding an element to an array, updating an element in an array, and removing an element from an array. Immutable simply means unchangeable or "read-only". Rather than mutating the array directly, it's best to copy the array first and then only mutate the copy.

I use these techniques very often in React, when updating state with hooks such as useState and useReducer. I've also used these techniques when writing reducers with state management libraries such as Redux. Any updates to state must always be done immutably.

Consider the following array of products that we will start with.

const products = [
  { name: "Sofa", price: 1000 },
  { name: "Speakers", price: 700 },
];

Performing an immutable add

We should avoid using the push() array method to add a new item to the array because it mutates the array directly rather than performing an immutable update.

products.push({
  name: "TV", 
  price: 100,
});

Instead of using push(), we'll use the array spread ... operator to copy the existing products array into a new array, and then add our new product to the new array.

const updatedProducts = [
  ...products,
  { name: "TV", price: 100 },
];

The updatedProducts will now look like the following.

[{
  name: "Sofa",
  price: 1000
}, {
  name: "Speakers",
  price: 700
}, {
  name: "TV",
  price: 100
}]

What if we need to insert the new product, "TV" in this case, into a specific position of the array? How can we do that? Let's take a look.

const updatedProducts = [
  // products before the insertAt index
  ...products.slice(0, insertAt),
  // New item:
  { name: "TV", price: 100 },
  // products after the insertAt index
  ...products.slice(insertAt)
];

If we hard-code insertAt to 1 for this example, here's what updatedProducts looks like.

[{
  name: "Sofa",
  price: 1000
}, {
  name: "TV",
  price: 100
}, {
  name: "Speakers",
  price: 700
}]

The new product, "TV", has been added to the second position of the array. Remember, arrays start at an index of 0 and we used an insertAt index of 1, giving us the second array position.

Performing an immutable delete

To delete items from an array in an immutable way, we can use the array filter() method to filter out the elements that we want to remove. The filter() method returns a new array with those elements filtered out.

const products = [
  { name: "Sofa", price: 1000 },
  { name: "TV", price: 100 },
  { name: "Speakers", price: 700 },  
];
const toRemove = "TV";
const updatedProducts = products.filter(({ name }) => name !== toRemove);

Here's what updatedProducts looks like after the filter() operation.

[{
  name: "Sofa",
  price: 1000
}, {
  name: "Speakers",
  price: 700
}]

Performing an immutable update

To update items in an array in an immutable way, we can use the array map() method. map() allows us to iterate over an array and applies the function that we provide to it. In this function that we provide to map, we can target specific array items and update them. map() then returns a new array containing the updates we performed.

const products = [
  { name: "Sofa", price: 1000 },
  { name: "TV", price: 100 },
  { name: "Speakers", price: 700 },  
];

const updatedProducts = products.map(product => {
  if (product.name === 'TV') {
    // No change
    return product;
  } else {
    // Put other products on sale
    return {
      ...product,
      price: product.price - 100,
    };
  }
});

In this example, we put every product except for "TV" on sale with a rebate of 100 dollars.

We use conditions in the function provided to map() to target the products to discount. Items that are not on sale are simply returned. Items that are on sale get their details copied into a new object using the JavaScript spread operator. We then apply the discount on the price field, overwriting the price that we copied in to the new object with the spread operator. Finally, we return the new object. The map() method then returns a new array containing the new objects for the discounted products.

Here's what updatedProducts looks like after the map() operation.

[{
  name: "Sofa",
  price: 900
}, {
  name: "TV",
  price: 100
}, {
  name: "Speakers",
  price: 600
}]

Performing an immutable sort

The JavaScript sort() method mutates the original array, so we can't use it directly if we want the sort to be immutable. Instead, we should copy the original array first and then sort it.

const products = [
  { name: "Sofa", price: 1000 },
  { name: "TV", price: 100 },
  { name: "Speakers", price: 700 },  
];
const sortedproducts = [...products];
sortedproducts.sort((a, b) => a.price - b.price);

This ascending sort operation on product prices will result in the following.

[{
  name: "TV",
  price: 100
}, {
  name: "Speakers",
  price: 700
}, {
  name: "Sofa",
  price: 1000
}]

Conclusion

We looked at techniques to update an array of objects in an immutable way using the JavaScript spread operator ..., the array filter() method, the array map() method, and the array sort() method.

Whenever possible, perform array updates in an immutable way using the techniques that we covered. Perform array updates immutably especially when updating state or when updating arrays received as function parameters. This will keep the original array intact and prevent undesired results.