{ ...obj } looks like it copies an object. It does — partially. The outer object is duplicated; everything inside is still shared. That distinction is the source of more "I changed a and somehow b changed too" bugs than I can count.
This is the article version of "what ... actually does" and when one spread isn't enough.
Shallow Copy Copies The Container
A shallow copy duplicates the outer level but keeps references to everything nested.
const original = {
name: 'Alice',
address: { city: 'Kyiv' },
};
const copy = { ...original };
copy.name = 'Bob';
copy.address.city = 'Lviv';
console.log(original.name); // 'Alice' — primitives copied
console.log(original.address.city); // 'Lviv' — nested object SHARED
copy.name and original.name are independent because strings are values. copy.address and original.address are the same object because objects are references. Mutating one mutates both.
Object.assign({}, original) does the same thing. [...arr] does the same thing for arrays. All shallow.
Nested Objects Stay Shared
The bug pattern in real code:
function increment(state) {
const next = { ...state };
next.counters.value++; // 🐛 mutates state.counters too
return next;
}
// store thinks state didn't change (same counters reference)
// React doesn't re-render
// counter goes up but UI doesn't update
The fix is to spread every level you're modifying:
function increment(state) {
return {
...state,
counters: { ...state.counters, value: state.counters.value + 1 },
};
}
This is verbose but correct. The pattern grows linearly with nesting depth — three levels of nesting, three levels of spread. Past about three levels, helpers like Immer make this much more readable.
Deep Copy Has Trade-Offs
Sometimes you genuinely need an independent copy of everything — for snapshots, undo state, debugging, sending to a worker. The right tool in 2026 is structuredClone:
const copy = structuredClone(original);
copy.address.city = 'Lviv';
console.log(original.address.city); // 'Kyiv' — fully independent
structuredClone is a built-in browser and Node API that handles:
- Plain objects and arrays
Date,Map,Set,RegExpArrayBuffer, typed arrays,Blob,File- Circular references (without infinite recursion)
It does not handle:
- Functions (throws)
- DOM nodes (throws)
- Class instances → returns plain objects, loses prototype
- Symbols as keys (silently dropped)
For most data shapes, it's the right answer. For class-heavy data with methods, you may need a custom clone or a library.
JSON Copy Is A Trap
The pattern you'll still see in old code:
const copy = JSON.parse(JSON.stringify(original)); // ⚠ lossy
It "works" for plain objects, but:
Datebecomes a stringundefined, functions, symbols are droppedMap,Set,RegExpbecome empty{}NaN,Infinitybecomenull- Throws on circular references
- Two
Dateround-trips silently differ from the original
Use structuredClone instead. The JSON.parse(JSON.stringify(...)) trick has been obsolete since structuredClone shipped widely.
When To Reach For Each
A short decision rule:
| Need | Use |
|---|---|
| Update one level only | { ...obj, key: value } |
| Update nested path | spread at every level, or Immer |
| Full independent snapshot | structuredClone(obj) |
| Send to worker / serialize | structuredClone (worker uses it under the hood for postMessage) |
| Class instances with methods | manual clone or library |
Most production code uses the first two. The third comes up for snapshots, undo state, and copying data crossing async boundaries.
Pro Tips
- Reach for
structuredClone— it's built-in, fast, and correct. - Spread at every level you're modifying — shallow copies leak state references.
- Use Immer for deep nested updates — same result, much less ceremony.
- Avoid
JSON.parse(JSON.stringify(...))— silent data loss. - Test mutations — write a quick assertion that the original wasn't touched.
Final Tips
The bug pattern this prevents is one of the sneakiest in JavaScript: action at a distance. You think you cloned an object, the code looks correct, but the original mutates anyway because the inner thing was shared. The user reloads. The dev shrugs.
Spread is shallow. structuredClone is deep. Use the one that matches your real intent.
Good luck — and may your nested updates never bleed 👊




