A junior developer on my team once filed a bug report titled “Editing a user’s profile also changes another user’s profile.” He had written const copy = {...originalUser}, changed copy.address.city, and watched in horror as originalUser.address.city changed too. The spread operator had copied the user object, but the nested address object was shared between the original and the copy.
I have seen this exact bug more times than I can count. A developer copies an object with spread, modifies a nested field, and the original changes unexpectedly. It is not a JavaScript quirk — it is how object references work. Spread is one of the most convenient features in modern JavaScript. It creates a new object or array, copies over the properties, and lets you override specific values in one concise expression. But it only copies one level deep. Understanding where spread works and where it falls short will save you from bugs that are hard to reproduce and harder to debug.
Short version: Spread copies values at the top level. Primitives (strings, numbers, booleans) are safe because their value is the data itself. Objects and arrays are shared because spread copies the reference, not the nested data.
What Spread Actually Does
When you write {...original}, JavaScript iterates over the enumerable own properties of original and assigns each value to the new object. If the value is a primitive, the new object gets its own copy of that value. If the value is an object (including arrays, Date instances, or nested objects), the new object gets a reference that points to the same memory location as the original.
const original = { name: 'Alice', address: { city: 'Jakarta' } }
const copy = { ...original }
copy.name = 'Bob' // original.name is still 'Alice' ✔️
copy.address.city = 'Surabaya' // original.address.city is now 'Surabaya' ❌This behavior is not a bug. It is how JavaScript objects work. The spread operator copies the value of each property, and for objects, the value is a reference. The same rule applies to arrays: [...arr] creates a new array, but if the array contains objects, those objects are shared.
When Spread Works and When It Does Not
Spread is perfectly safe when your data is flat — when every value is a primitive or when you only need to modify top-level properties. The key word is flat: if your object has no nested objects or arrays, spread creates a fully independent copy because primitives (strings, numbers, booleans) are copied by value, not by reference. I use it constantly for merging config objects: const config = {...defaults, ...userOverrides} creates a clean separation between default values and overrides without mutating either source. It is also safe for adding or removing items at the top level of an array: [...items, newItem] or items.filter(item => item.id !== id).
Spread fails when you need to modify a nested property without affecting the original. If you write const copy = {...state} and then change copy.user.profile.name, you have mutated the original because the nested profile object is shared. The correct approach is to spread at every level:
const newState = {
...state,
user: {
...state.user,
profile: {
...state.user.profile,
name: 'Bob'
}
}
}This is the standard immutable update pattern in React and Redux. The deeper your nesting, the more verbose the code becomes — and that verbosity is intentional. It forces you to be explicit about which parts of the state are changing. If the nesting feels excessive, it is a sign that your data structure might be too deep and could benefit from normalization.
Arrays Inside Objects
Arrays inside objects behave the same way. Spread the object, then spread the array:
const state = { items: [1, 2, 3], meta: { count: 3 } }
const newState = {
...state,
items: [...state.items, 4],
meta: { ...state.meta, count: 4 }
}This pattern is predictable and testable. Each spread expression is explicit about which part of the data is being copied and which part is being modified.
Deep Copy Alternatives
When you need a full deep copy and nested spread is too tedious, you have three options:
structuredClone is the modern, native solution available in all modern browsers and Node.js 17+. It handles Date, Map, Set, RegExp, ArrayBuffer, and circular references. It also works with nested objects and arrays perfectly. Use it unless you need to support very old browsers.
const deepCopy = structuredClone(original)
// Completely independent at every nesting levelJSON round trip (JSON.parse(JSON.stringify(obj))) works for plain objects and arrays but fails for Date, Map, Set, undefined, BigInt, and circular references. I used this approach for years before structuredClone existed, and it caused countless bugs with Date objects becoming strings.
Lodash cloneDeep is a third-party option that handles most edge cases including custom class instances and prototype chains. It adds a dependency but provides consistent behavior across environments. I prefer structuredClone now that it is widely available.
Real-World Examples
I once spent a full afternoon debugging a dashboard where editing a user’s role in the admin panel also changed the role in the user’s own active session.
The admin would add a permission, and the user would see it appear instantly without refreshing. It looked like a real-time sync feature — except it was a bug. The code used {...state} to copy the user object, but the nested permissions array was shared. Changing one changed the other.
The fix was replacing the shallow spread with a deep copy using structuredClone. One line change. Hours of debugging saved.
Another case I ran into was a form editor that let users add and remove items from a list before saving. The implementation used spread to create a working copy, then pushed and spliced items directly. The undo feature compared the current state against the original to detect changes. Since spread only copied the top-level array reference, mutating nested objects modified the original.
Switching to structuredClone for the initial copy solved it instantly.
These two experiences taught me a simple rule. For flat data like a list of strings or a one-level config object, spread is adequate. For anything with nested objects or arrays, especially if you plan to mutate or compare the copy against the original later, use structuredClone and move on.
e code readable.