What exactly does the spread syntax copy in `const copy = [...items]` or `const next = {...state}`, and what bug appears once the data has a nested level?
answer
- container new, contents shared
- one level, never recursive
- nested reference is identical
- identity check sees no change
basics
~20 sSpread produces a one-level copy: the new array or object is a fresh container, but every nested array or object inside it is the very same reference as in the original. Mutating a nested value is visible through both.
solid answer
~50 sSpread copies values one level deep. `[...items]` builds a new array and `{...state}` a new object, so adding, removing or replacing a top-level element or key on the copy leaves the original untouched. But each copied value is transferred as-is, and for objects and arrays the value *is* the reference — so `copy[0]` and `items[0]` point at the same object. `next.user.name = 'x'` therefore changes `state.user.name` too, and any equality check on `state.user` still sees the same identity. This is the classic broken "immutable update": people spread the outer object, mutate a nested field, and then wonder why change detection did not fire or why an undo snapshot changed retroactively. The fix is to spread every level you intend to modify — `{...state, user: {...state.user, name: 'x'}}` — or to deep-copy when the shape is arbitrary.
code
javascript · 11 linesconst state = { count: 0, user: { name: 'ada' } };
const broken = { ...state };
broken.user.name = 'grace';
console.log(state.user.name); // 'grace' — original mutated
console.log(broken.user === state.user); // true
const fresh = { count: 0, user: { name: 'ada' } };
const fixed = { ...fresh, user: { ...fresh.user, name: 'grace' } };
console.log(fresh.user.name); // 'ada' — untouched
console.log(fixed.user === fresh.user); // falsego deeper
Know that spread makes a copy that is only one level deep, and be able to say that copy[0] === original[0] is true for nested objects. Recognize the syntax for both arrays and objects.
Explain why the copy is shallow — values are copied, and an object value is a reference — and write the layered spread that correctly updates a nested field without mutating the source.
Diagnose the symptom in the wild: a component that will not re-render, a snapshot that changed retroactively, an audit log holding the new value. Trace it to a nested mutation behind a shallow copy and argue for flatter state or a real deep copy.
Own the tradeoff between hand-rolled layered spreads, a deep-copy step, and a data model shallow enough that the question stops arising. Set the convention so that immutability is not left to every author remembering to spread each level.
## One level, always exactly one Spread evaluates the source and copies its values into a brand-new container. For an array, `[...items]` iterates the source and appends each produced value to a fresh array. For an object, `{...state}` copies the source's own enumerable properties onto a fresh object. In both cases the *values* are copied — and in JavaScript a variable or property holding an object holds a reference to it, not the object itself. Copying the value therefore copies the reference. ```js const items = [{ id: 1 }, { id: 2 }]; const copy = [...items]; copy === items; // false — new array copy[0] === items[0]; // true — same inner object copy.push({ id: 3 }); // items is unaffected: length still 2 copy[0].id = 99; // items[0].id is now 99 as well ``` That pair of results is the whole concept: the *container* is independent, the *contents* are shared. ## Why this shows up as a state bug The idiom that made spread ubiquitous is the immutable update — produce a new value instead of mutating the old one, so that anything comparing the before and after by identity can tell something changed. Spread does that correctly for the level you spread, and silently fails for anything below it: ```js const state = { count: 0, user: { name: 'ada', prefs: { theme: 'dark' } } }; // Correct at the top level const a = { ...state, count: 1 }; a !== state; // true — a genuinely new object // Broken one level down const b = { ...state }; b.user.name = 'grace'; state.user.name; // 'grace' — the original mutated b.user === state.user; // true — nothing looks changed to an identity check ``` Two distinct symptoms follow. First, the "old" value is retroactively corrupted, which destroys undo history, memoized snapshots and anything that logged the previous state. Second, code that compares by reference concludes nothing changed, because `b.user` and `state.user` are the same object — the mutation is invisible to the very mechanism the copy existed to serve. ## Copying the path you touch The standard fix is to spread each level along the path being updated, leaving untouched branches shared (which is fine, because they are never mutated): ```js const next = { ...state, user: { ...state.user, prefs: { ...state.user.prefs, theme: 'light' } }, }; next !== state; // true next.user !== state.user; // true next.user.prefs.theme; // 'light' state.user.prefs.theme; // 'dark' — untouched ``` For arrays of objects the same rule applies: `[...list]` is enough to insert or remove an entry, but replacing a field inside one entry needs a new object for that entry, typically produced with `map` returning `{ ...entry, done: true }` for the matching item. When the shape is arbitrary or deeply nested, hand-spreading every level becomes unreadable and error-prone; that is the point at which a real deep copy, or a data model that keeps nesting shallow, is the better answer. ## Things people wrongly expect spread to do - **Recurse.** It never does, at any depth, for arrays or objects. - **Preserve holes.** `[...[1, , 3]]` yields `[1, undefined, 3]`: the array iterator produces `undefined` for a hole, so the copy is dense. - **Merge nested keys.** `{...a, ...b}` replaces a shared key's value wholesale with `b`'s; it does not merge two nested objects. - **Copy non-enumerable or inherited properties.** Object spread takes only own enumerable properties. ## Answering it well State the rule in one sentence (new container, shared contents), show the `copy[0] === items[0]` check that proves it, then name the concrete failure: a nested mutation that both corrupts the original and defeats identity-based change detection. Finishing with the layered-spread fix and the honest caveat that it stops scaling at three or four levels is what turns a definition into an answer.
- After `const next = {...state}`, which identity comparisons are true and which are false?`next === state` is false — a fresh object was created. Every nested comparison such as `next.user === state.user` is true, because the reference was copied verbatim. That asymmetry is precisely why top-level updates work and nested ones do not: the change-detection layer only sees a difference at the level you actually rebuilt.
- How would you update one field of one object inside an array of objects without mutating anything?Rebuild the array and the one entry you touch: `list.map(item => item.id === id ? { ...item, done: true } : item)`. `map` returns a new array, and only the matching entry is a new object; the untouched entries are shared by reference, which is safe as long as nothing mutates them.
- Why does spreading an array with holes not preserve them?Array spread consumes the source through its iterator, and the array iterator visits every index from 0 to length and produces `undefined` for indexes with no own property. The result is therefore dense: `[...[1, , 3]]` is `[1, undefined, 3]` with three real elements, not a hole in the middle.
saying these in an interview costs you the question
- Calls spread a deep copy
- Expects nested objects to be cloned too
- Thinks a new outer object protects inner values
- Believes {...a, ...b} deep-merges shared keys
- Assumes spread copies inherited or non-enumerable properties