AniUI Academy

Destructuring and Spread

Destructuring, defaults, rest and spread across arrays, objects and parameters — plus the shallow-copy trap where editing your copy quietly rewrites the original.

13 min read

The expensive bug here is not a syntax error. It is copying an object, editing the copy, and finding that the original changed too. Destructuring and spread read so cleanly that you end up trusting them further than they go.

Pulling values out of arrays

Position decides what you get. Holes are allowed, and a default covers a missing slot.

const scores = [88, 74];
 
const [first, second, third = 0] = scores;
// 88, 74, 0
 
const [, runnerUp] = scores; // skip the first

You have used this without noticing every time you wrote const [value, setValue] = useState(). It is why hook results are arrays: you choose the names.

Swapping needs no temporary variable. Start the line with a semicolon, since the previous line might otherwise join up with the bracket:

let current = "draft";
let next = "published";
 
;[current, next] = [next, current];

Pulling values out of objects

Name decides what you get, so order is irrelevant.

const lesson = {
  title: "Modules",
  minutes: 11,
  author: { name: "Priya", city: "Chennai" },
};
 
const { title, minutes } = lesson;
const { title: heading } = lesson;                 // rename
const { level = "intermediate" } = lesson;         // default
const { author: { name, city } } = lesson;         // nested
const { author: { country = "IN" } = {} } = lesson; // nested with a guard

Two things to be precise about. Renaming reads backwards the first few times: title: heading means take title, call it heading. And defaults fire only on undefined — a stored null will come through as null and break the code you expected the default to protect.

Nested destructuring throws if the middle object is missing, which is why that last line adds = {}. Past two levels, stop and use optional chaining instead.

Destructuring parameters

This is the pattern worth internalising. Compare a positional signature with an options object:

createLesson("Modules", 11, true, false, "intermediate");
 
createLesson({
  title: "Modules",
  minutes: 11,
  published: true,
  level: "intermediate",
});

The second is readable at the call site and survives new arguments being added. Destructure it in the signature with defaults, and give the whole parameter a default so calling with no arguments still works:

function createLesson({
  title,
  minutes = 10,
  published = false,
  level = "beginner",
} = {}) {
  return { title, minutes, published, level };
}

The same trick makes callbacks tidier:

users.map(({ name, email }) => `${name} <${email}>`);

Rest: collecting what is left

In destructuring, ... gathers the remainder:

const [featured, ...others] = lessons;
 
const { password, ...safeUser } = user; // omit a field from a copy

That second line is the standard way to strip a property without mutating — handy before logging a user record or sending it to the client.

In a function signature, ... collects extra arguments into a real array:

function track(event, ...details) {
  console.log(event, details.join(", "));
}
 
track("lesson_started", "closures", "13min");

Rest must be last in both cases.

Spread: copying and merging

The same three dots in the other direction, spreading a value out.

const base = [1, 2];
const more = [...base, 3];              // [1, 2, 3]
const joined = [...base, ...more];      // [1, 2, 1, 2, 3]
 
const defaults = { theme: "light", fontSize: 14 };
const settings = { ...defaults, theme: "dark" }; // later keys win

Order matters for objects: the last occurrence of a key wins, so put overrides after defaults. Spread also works for arguments — Math.max(...scores) — and turns any iterable into an array, including a Set, which is the one-line way to remove duplicates: [...new Set(tags)].

Two details worth knowing. Spreading undefined into an object is a no-op rather than an error, which makes conditional properties tidy:

const query = {
  page: 1,
  ...(search ? { q: search } : {}),
};

And object spread is not quite Object.assign. Both copy own enumerable properties, but Object.assign writes into an existing object and triggers any setters on the target, while spread builds a fresh plain object. When you only want a copy, spread is the safer default.

Rest and spread combine naturally in a function that takes options:

function createLesson({ title, ...rest }) {
  return { title, slug: title.toLowerCase().split(" ").join("-"), ...rest };
}

Try it yourself

Try it yourself
Loading playground...

Every one of these copies is shallow

Spread copies the values of the top-level properties. When a value is an object or an array, the value is the reference. Both objects then point at the same nested thing.

const defaults = { theme: "light", editor: { fontSize: 14 } };
const user = { ...defaults, theme: "dark" };
 
user.editor.fontSize = 18;
 
console.log(defaults.editor.fontSize); // 18 — the original changed
console.log(user.editor === defaults.editor); // true

This is the bug that eats afternoons. You reset a form to its defaults and the defaults have already been edited. You clone a cart line to preview a discount and the real cart shows the discount. In React it is worse in a quieter way: you mutate a nested object inside a copied state object, the top-level reference is new so a re-render happens, and the change appears to work — until a component that compares the nested object decides nothing changed.

There are two honest ways out. Copy every level you intend to touch:

const user = {
  ...defaults,
  editor: { ...defaults.editor, fontSize: 18 },
};

Or take a real deep copy when the shape is unknown or deep:

const snapshot = structuredClone(defaults);

structuredClone handles nested objects, arrays, Map, Set and Date. It throws on functions and DOM nodes. The old JSON.parse(JSON.stringify(x)) trick works but silently turns dates into strings and drops undefined, so prefer structuredClone.

Immutable updates

React state, reducers and most state libraries require a new reference rather than an edit in place. These four cover nearly everything.

// add
const withNew = [...items, newItem];
 
// remove
const without = items.filter((item) => item.id !== id);
 
// update one item
const updated = items.map((item) =>
  item.id === id ? { ...item, done: true } : item
);
 
// update a nested field
const next = {
  ...order,
  customer: { ...order.customer, city: "Pune" },
};

Note the third one. Copying the array alone is not enough — [...items] gives you a new array holding the same item objects, so mutating one of them is still visible everywhere. Replace the item, do not edit it.

What to remember

  • Destructuring names things at the point of use; defaults apply only to undefined.
  • The options object with destructured parameters and defaults is the signature to reach for.
  • Rest collects, spread expands, and rest must come last.
  • Spread copies one level deep. Copy each level you change, or use structuredClone.

Check yourself

4 questions · pass 3/4 to unlock Modules

up to 50
  1. 1.You spread an object that has a nested prefs object, then set copy.prefs.theme = "light". What is original.prefs.theme now?

  2. 2.When does the default in const { total = 0 } = order apply?

  3. 3.Which is true of rest in function tag(first, ...rest) {}?

  4. 4.You need to mark one item in a state array as done without mutating the array. Which is right?

4 left to answer