Destructuring, Spread, and Rest

Pulling values out of objects and arrays, and copying them without mutation.

What you'll learn

  • Destructuring objects and arrays, with defaults and renaming
  • The difference between rest (collecting) and spread (expanding)
  • How to update data by copying instead of mutating
  • Why "copying instead of mutating" is a hard requirement in React

Destructuring objects

Destructuring unpacks properties into variables in one step. It is the reason React props read the way they do.

const user = { name: "Ada", role: "engineer", city: "London" };

const { name, role } = user;
console.log(name, role);

// Rename while unpacking, and supply a default for a missing property.
const { city: location, country = "UK" } = user;
console.log(location, country);

The default only applies when the property is undefined — not when it is null, 0, or an empty string. That distinction catches people out.

Destructuring in parameters

You can destructure directly in a parameter list. This is how almost every React component receives its props:

// Instead of reaching into an object inside the function...
function greetLong(user) {
  return "Hello, " + user.name;
}

// ...unpack it in the signature.
function greet({ name, greeting = "Hello" }) {
  return greeting + ", " + name;
}

console.log(greetLong({ name: "Ada" }));
console.log(greet({ name: "Grace" }));
console.log(greet({ name: "Alan", greeting: "Hi" }));

Destructuring arrays

Arrays destructure by position rather than name, so you choose the names:

const scores = [90, 80, 70];

const [first, second] = scores;
console.log(first, second);

// Skip positions with a hole, and collect the remainder with rest.
const [, , third] = scores;
console.log("third:", third);

This is exactly the shape of const [count, setCount] = useState(0)useState returns a two-element array, and you name both parts at the call site.

Rest and spread

They look identical — three dots — but they work in opposite directions.

Rest collects the leftovers into a new variable. It goes on the left of an assignment, or last in a parameter list:

const { name, ...details } = { name: "Ada", role: "engineer", city: "London" };
console.log(name);
console.log(details);

function sum(...numbers) {
  return numbers.reduce((total, n) => total + n, 0);
}

console.log(sum(1, 2, 3, 4));

Spread expands a value into somewhere else. It goes on the right:

const base = { name: "Ada", role: "engineer" };

// Copy, then override. Later keys win.
const promoted = { ...base, role: "lead" };

console.log(base);
console.log(promoted);

const low = [1, 2];
const all = [...low, 3, 4];
console.log(all);

Note that base is untouched. That is the whole point.

Copying instead of mutating

This is the habit the rest of the course depends on. React decides whether to re-render by comparing values by identity: if you mutate an object in place, it is still the same object, and React sees no change.

const original = { name: "Ada", role: "engineer" };

const mutated = original;
mutated.role = "lead";

// Same object, so the "original" changed too.
console.log("same object:", original === mutated);
console.log(original);

const copied = { ...original, role: "principal" };
console.log("same object:", original === copied);
console.log(original.role, "vs", copied.role);

The same applies to arrays. Reach for the methods that return a new array (map, filter, concat, spread) rather than the ones that change it in place (push, splice, sort, reverse):

const numbers = [3, 1, 2];

const mutating = numbers;
mutating.push(4); // changes `numbers` as well
console.log("after push:", numbers);

const copiedAndSorted = [...numbers].sort();
console.log("copy sorted:", copiedAndSorted);
console.log("original:   ", numbers);

One level deep

Spread makes a shallow copy. Nested objects are still shared, so updating a nested value means copying at each level you change:

const user = { name: "Ada", address: { city: "London" } };

const shallow = { ...user };
shallow.address.city = "Paris"; // still the same inner object

console.log("leaked:", user.address.city);

const proper = {
  ...user,
  address: { ...user.address, city: "Berlin" },
};

console.log("original:", user.address.city);
console.log("updated: ", proper.address.city);

Common mistakes

  • Expecting a default to fill in null. Defaults only apply to undefined.
  • Confusing rest with spread. Left side collects, right side expands.
  • Trusting a shallow copy. { ...user } shares every nested object.
  • Sorting without copying. sort and reverse change the array in place.

Exercise

addTag currently mutates the post it is given, so original changes too. Rewrite it to return a new post with the tag appended, leaving original untouched. Both logged tag lists should differ when you are done.

function addTag(post, tag) {
  post.tags.push(tag);
  return post;
}

const original = { title: "Hello", tags: ["intro"] };
const updated = addTag(original, "react");

console.log("original:", original.tags);
console.log("updated: ", updated.tags);