Array.prototype.with() - Improved JS array updates

js

TC39 introduced Array.prototype.with() to Javascript in 2023. It's a relatively new method for replacing an element in an array. It's been available in Node since v20.

It's non-mutating and returns a new array with the specified element replaced. It's a much more ergonomic way to update arrays compared to using methods like slice(), map() or using spread syntax. I feel like most devs still reach for older ways to update arrays which unless your use to Javascript looks confusing.

I really wish it was named replaceWith() though!

Basic use


It's use pretty straightforward.

const snacks = ["ice cream", "tea", "coffee", "cookies"];
const newSnacks = snacks.with(1, "hot chocolate");
console.log(newSnacks); // ["ice cream", "hot chocolate", "coffee", "cookies"]

You can also use negative indexes.

const snacks = ["ice cream", "tea", "coffee", "cookies"];
const newSnacks = snacks.with(-1, "brownies");
console.log(newSnacks); // ["ice cream", "tea", "coffee", "brownies"]

Updating by index like so snacks[1] = "hot chocolate" mutates the array.

Not mutating arrays is important because without being careful with the update's scope the update can cause bugs and side effects. This commonly causes issues in React where mutating state can cause bugs in rendering or state not having expected values.

It's also handy for the method to return a new array because it allows you to chain it with other methods like map() or filter() and be available for further operations like adding it to a request payload or diffing with the original value.

Before with() updating an array without mutating it wasn't so straightforward. Especially if a developer isn't used to Javascript.

// non-mutating with spread and slice.
const snacks = ["ice cream", "tea", "coffee", "cookies"];
const newSnacks = [...snacks.slice(0, 1), "hot chocolate", ...snacks.slice(2)];
console.log(newSnacks); // ["ice cream", "hot chocolate", "coffee", "cookies"]

With React


Here's a simple example with React for replacing the last player without mutating state.

const [players, setPlayers] = useState(["Alice", "Bob", "Charlie"]);
 
setPlayers((prev) => prev.with(-1, "Diana")); // ['Alice', 'Bob', 'Diana']

Much cleaner than with spread syntax.

const [players, setPlayers] = useState(["Alice", "Bob", "Charlie"]);
setPlayers((prev) => [...prev.slice(0, -1), "Diana"]); // ['Alice', 'Bob', 'Diana']

Using it with other methods


It's often used with findIndex(). Here's an example of updating it to a user's favorite Japanese food.

const favoriteJapaneseFoods = [
  { name: "Alice", favoriteFood: "sushi" },
  { name: "Bob", favoriteFood: "ramen" },
  { name: "Charlie", favoriteFood: "udon" },
];
 
const index = favoriteJapaneseFoods.findIndex((f) => f.name === "Bob");
const updatedFoods = favoriteJapaneseFoods.with(index, {
  ...favoriteJapaneseFoods[index],
  favoriteFood: "hitsumabushi",
});
console.log(updatedFoods);
// [
//   { name: "Alice", favoriteFood: "sushi" },
//   { name: "Bob", favoriteFood: "hitsumabushi" },
//   { name: "Charlie", favoriteFood: "udon" },
// ]

Swapping items in an array


I'm not sure how useful this example is but I thought it was an interesting use of at() and with() together. This would be useful in a more complex use case for rearranging items.

const items = ["takoyaki", "yakitori", "taiyaki", "yakiniku", "ramen"];
 
const swapped = items.with(0, items.at(-1)).with(-1, items.at(0));
console.log(swapped); // ["ramen", "yakitori", "taiyaki", "yakiniku", "takoyaki"]

MDN docs for with()
TC39 proposal for with()