Spread can be a timebomb
I have, a few times in the passed, used the spread syntax (…) in JavaScript in a way that has later caused a very hard-to-debug error in my code. Essentially, I’ve used the syntax on a single level array or object only to later increase it’s depth—While spread remains a shallow copy.
This article isn’t to teach you how to use spread, so I’ll keep this part light. Below are a few examples of using spread.
Let array1 = [1, 2, 3];
Let array2 = […array1];
Let object1 = {
property1: true,
property2: "red"
};
let object2 = {…object1};
let standardMeal = {
healthy: false,
tasty: true,
servings: 1
}
let breakfast = {
…standardMeal,
ingredients = ["oats", "muesli", "milk"]
}
In each case, the spread operator (…) spreads it’s properties into the new object or array. So…
// array2 becomes:
[1, 2, 3]
// object2 becomes:
{
property1: true,
property2: "red"
};
// breakfast becomes
{
healthy: false,
tasty: true,
servings: 1,
ingredients = ["oats", "muesli", "milk"]
}
The problem
All of the properties that we’ve spread so far, however, are primitive values (strings, numbers, booleans), so their values are duplicated as new properties. This means if we change array1, it won’t affect array2. Likewise for object1 or standardMeal. The values were copied and no long maintain a link.
The problem arises, when one of the parameters being spread is not a primitive. In this instance, a reference to that object variable is copied into the new object.
For instance, if we were to do the same thing with the breakfast variable, we would get something conceptually similar to this.
// What we did
let breakfast = {
healthy: false,
tasty: true,
servings: 1,
ingredients = ["oats", "muesli", "milk"]
}
Let tuesdayBreakfast = {
…breakfast,
}
// What we’d get in Tuesday breakfast
{
healthy: false,
tasty: true,
servings: 1,
ingredients = <reference-to-ingredients-in-breakfast>
}
If we accessed tuesdayBreakfast’s ingredients, we would still see the ingredients as usual. It would appear successful. But what we actually have is not an original duplicate of the data, but a reference to the original data.
This Means if we change the ingredients in the breakfast variable, it would also affect the tuesdayBreakfast variable and vice versa.
Note that this is the same way non-primitive variables work in general. If I define an object and then set another variable to point at it. That’s what it does. It points at it (or creates a reference to it).
Let firstObj = { Name: ‘Dale’, Height: ‘182cm’ } Let secondObj = firstObj; // What we get secondObj = <reference-to-firstObj>
Spread is a slippery slope
I absolutely love the spread syntax. The purpose of this article isn’t to dissuade you from using it. Rather, it’s important to know where it might break so you don’t go chasing ghost bugs in your program.
Because of this, if I know that I want to create a complete duplicate of an object, even if that object is only a single level object or array, I now always use JSON.parse(JSON.stringify(object)).
This might feel a little overkill and harder to read, but it means that even if the original object grows in complexity, I know that the setup won’t break and create confusing errors.
Is this less efficient? Maybe, I don’t know. But optimising can (and should), be done later when your program settles close to it’s more permanent setup—So you can come back to it then if necessary.
I also discuss JSON.parse in my article Deep Cloning Efficiently if you want to understand it better.
That’s it
Thanks…
I also dissect and speculate on design and development.
Digging into subtle details and implications, and exploring broad perspectives and potential paradigm shifts.
Check out my conceptual articles on Substack or find my latest below.
You can also find me on Threads, Bluesky, Mastodon, or X for more diverse posts about ongoing projects.
Leave a Reply