{ ...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.

JavaScript
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:

JavaScript
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:

JavaScript
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.

Object copy diagram showing the bug. Spread copy on the left duplicates the outer wrapper but the inner address object is still shared between original and copy — so mutating one bleeds into the other. structuredClone on the right makes both layers independent.
Spread copies one layer. structuredClone copies all of them.

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:

JavaScript
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, RegExp
  • ArrayBuffer, 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:

JavaScript
const copy = JSON.parse(JSON.stringify(original)); // ⚠ lossy

It "works" for plain objects, but:

  • Date becomes a string
  • undefined, functions, symbols are dropped
  • Map, Set, RegExp become empty {}
  • NaN, Infinity become null
  • Throws on circular references
  • Two Date round-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

  1. Reach for structuredClone — it's built-in, fast, and correct.
  2. Spread at every level you're modifying — shallow copies leak state references.
  3. Use Immer for deep nested updates — same result, much less ceremony.
  4. Avoid JSON.parse(JSON.stringify(...)) — silent data loss.
  5. 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 👊