Skip to content

Destructuring and Spread

beginner18 min read

The Big Idea

Destructuring and spread are two of the most-used features in modern JavaScript. You'll see them in every React component, every API response handler, every utility function. They look like magic the first time, but once you see the pattern, they're dead simple.

Here's the deal: destructuring pulls values out of arrays and objects into individual variables. Spread does the opposite — it takes something apart and spreads its pieces into a new context. And they both use ..., which confuses everyone at first.

Mental Model

Think of a suitcase. Destructuring is unpacking — you open the suitcase and pull out specific items (shirt, laptop, charger) and place them on the table as separate things. Spread is dumping — you flip the suitcase upside down and pour everything out into a new suitcase, maybe adding a few extra items on top. Same suitcase, two different operations. The ... syntax is just the "everything else" gesture — whether that means "grab the rest" (destructuring) or "dump it all" (spread) depends on which side of the = you're on.

Array Destructuring

Instead of accessing array elements by index, you can pull them directly into named variables:

const rgb = [255, 128, 0];

// Without destructuring
const red = rgb[0];
const green = rgb[1];
const blue = rgb[2];

// With destructuring
const [r, g, b] = rgb;
console.log(r); // 255
console.log(g); // 128
console.log(b); // 0

The position matters. The first variable gets the first element, the second variable gets the second element, and so on.

Skipping Elements

Don't need every value? Leave a hole with a comma:

const scores = [92, 87, 95, 78];

const [first, , third] = scores;
console.log(first); // 92
console.log(third); // 95
// 87 and 78 are ignored

Default Values

If the array doesn't have enough elements, the variable gets undefined — unless you set a default:

const [a, b, c = 'fallback'] = [1, 2];
console.log(a); // 1
console.log(b); // 2
console.log(c); // "fallback" (no third element, default kicks in)

Defaults only activate when the value is undefined. Not null, not 0, not "" — only undefined:

const [x = 10] = [null];
console.log(x); // null (not 10 — null is not undefined)

const [y = 10] = [undefined];
console.log(y); // 10 (undefined triggers the default)

The Rest Element

Want the first few items separately and everything else in one go? That's the rest element:

const [winner, runnerUp, ...others] = ['gold', 'silver', 'bronze', 'fourth', 'fifth'];
console.log(winner);   // "gold"
console.log(runnerUp); // "silver"
console.log(others);   // ["bronze", "fourth", "fifth"]

The rest element must always be last. You can't put anything after it — JavaScript wouldn't know where "the rest" ends.

// This throws a SyntaxError
const [...first, last] = [1, 2, 3]; // SyntaxError: Rest element must be last
Quiz
What does this code log?

Swapping Variables

One of the cleanest tricks with array destructuring — swap two variables without a temp:

let x = 'hello';
let y = 'world';

[x, y] = [y, x];

console.log(x); // "world"
console.log(y); // "hello"

Object Destructuring

Same idea, but instead of position, you match by property name:

const user = { name: 'Sarah', age: 28, role: 'engineer' };

// Without destructuring
const name = user.name;
const age = user.age;

// With destructuring
const { name, age, role } = user;
console.log(name); // "Sarah"
console.log(age);  // 28
console.log(role); // "engineer"

Renaming Variables

What if the property name clashes with an existing variable, or you just want a better name? Use the colon syntax:

const response = { data: [1, 2, 3], status: 200, error: null };

const { data: items, status: httpStatus } = response;
console.log(items);      // [1, 2, 3]
console.log(httpStatus);  // 200
// console.log(data);     // ReferenceError — data is not defined

The syntax reads as "take the data property and assign it to a variable called items." The left side of the colon is the source property; the right side is the target variable.

Default Values

Just like arrays, defaults kick in when the property is undefined:

const config = { theme: 'dark' };

const { theme, language = 'en', fontSize = 16 } = config;
console.log(theme);    // "dark"
console.log(language); // "en" (not in config, default used)
console.log(fontSize); // 16 (not in config, default used)

You can combine renaming and defaults:

const { color: textColor = 'black' } = {};
console.log(textColor); // "black"

Rest in Objects

The rest syntax works with objects too — it collects all the properties you didn't explicitly destructure:

const { id, ...metadata } = { id: 1, name: 'Widget', price: 9.99, category: 'tools' };
console.log(id);       // 1
console.log(metadata); // { name: "Widget", price: 9.99, category: "tools" }

This is incredibly useful for separating "known" properties from "everything else" — a pattern you'll use constantly in React.

Quiz
What does textColor equal after this code runs?

Nested Destructuring

Real-world data is rarely flat. You can destructure nested structures by mirroring the shape:

const company = {
  name: 'Acme',
  address: {
    city: 'Portland',
    state: 'OR',
    zip: '97201'
  },
  employees: [
    { name: 'Alice', title: 'CTO' },
    { name: 'Bob', title: 'Engineer' }
  ]
};

const {
  name: companyName,
  address: { city, state },
  employees: [cto, engineer]
} = company;

console.log(companyName); // "Acme"
console.log(city);        // "Portland"
console.log(cto.name);    // "Alice"
console.log(engineer);    // { name: "Bob", title: "Engineer" }
Common Trap

When you destructure nested objects, the intermediate variable is NOT created. In the example above, address is not a variable — only city and state are. If you need address itself as a variable AND its nested properties, you need to destructure twice:

const { address, address: { city } } = company;
// Now both address and city are available

Don't go too deep with nested destructuring. If your destructuring pattern mirrors three or four levels of nesting, the code becomes harder to read than just accessing properties normally. One or two levels is the sweet spot.

Destructuring in Function Parameters

This is where destructuring truly shines. Instead of accepting an options object and picking properties off it inside the function, you can destructure right in the parameter list:

// Without destructuring
function createUser(options) {
  const name = options.name;
  const age = options.age;
  const role = options.role || 'viewer';
  // ...
}

// With destructuring
function createUser({ name, age, role = 'viewer' }) {
  console.log(name, age, role);
}

createUser({ name: 'Sam', age: 25 });
// "Sam" 25 "viewer"

This makes the function signature self-documenting — you can see exactly what properties the function expects without reading the body.

Combining with Default Parameters

There's a gotcha: if someone calls your function with no arguments at all, the destructuring fails because you can't destructure undefined:

function greet({ name = 'stranger' }) {
  console.log(`Hello, ${name}!`);
}

greet();          // TypeError: Cannot destructure property 'name' of undefined
greet({});        // "Hello, stranger!"
greet({ name: 'Kai' }); // "Hello, Kai!"

The fix is to default the entire parameter to an empty object:

function greet({ name = 'stranger' } = {}) {
  console.log(`Hello, ${name}!`);
}

greet();          // "Hello, stranger!" (no crash)
greet({});        // "Hello, stranger!"
greet({ name: 'Kai' }); // "Hello, Kai!"

The = {} at the end means "if no argument is passed, use an empty object" — then the inner defaults take over.

Quiz
What happens when you call processConfig() with no arguments?

Spread Operator in Arrays

The spread operator (...) takes an iterable (array, string, Set, etc.) and expands it into individual elements:

const nums = [1, 2, 3];
console.log(...nums); // 1 2 3 (three separate values, not an array)

Copying Arrays

Spread creates a shallow copy — a new array with the same elements:

const original = [1, 2, 3];
const copy = [...original];

copy.push(4);
console.log(original); // [1, 2, 3] (unchanged)
console.log(copy);     // [1, 2, 3, 4]

Merging Arrays

Combine arrays without concat:

const front = [1, 2];
const back = [3, 4];
const combined = [...front, ...back];
console.log(combined); // [1, 2, 3, 4]

// You can add elements in between
const withMiddle = [...front, 2.5, ...back];
console.log(withMiddle); // [1, 2, 2.5, 3, 4]

Converting Iterables

Spread works with any iterable — strings, Sets, Maps, NodeLists:

const chars = [..."hello"];
console.log(chars); // ["h", "e", "l", "l", "o"]

const unique = [...new Set([1, 2, 2, 3, 3, 3])];
console.log(unique); // [1, 2, 3]

Spread Operator in Objects

Object spread copies all enumerable own properties into a new object:

const defaults = { theme: 'light', fontSize: 14, lang: 'en' };
const userPrefs = { theme: 'dark', fontSize: 18 };

const config = { ...defaults, ...userPrefs };
console.log(config);
// { theme: "dark", fontSize: 18, lang: "en" }

Order Matters

Later properties override earlier ones. This is how "defaults with overrides" works:

// Defaults first, then user values override
const settings = { ...defaults, ...userSettings };

// This is WRONG — defaults would overwrite user settings:
const broken = { ...userSettings, ...defaults };

Adding and Overriding Properties

const user = { name: 'Alex', age: 30 };

// Add a new property
const withRole = { ...user, role: 'admin' };
// { name: "Alex", age: 30, role: "admin" }

// Override an existing property
const olderAlex = { ...user, age: 31 };
// { name: "Alex", age: 31 }

Shallow Copy Warning

Spread only copies one level deep. Nested objects are still shared references:

const original = {
  name: 'Team A',
  members: ['Alice', 'Bob']
};

const copy = { ...original };
copy.members.push('Charlie');

console.log(original.members); // ["Alice", "Bob", "Charlie"] — oops!

The members array in copy points to the same array as original.members. Spread didn't clone it — it just copied the reference. For nested data, you need a deep clone strategy (like structuredClone).

Why spread only does shallow copies

Spread uses the same mechanism as Object.assign — it iterates over the source object's own enumerable properties and copies them one by one. When a property's value is a primitive (number, string, boolean), the copy is independent. When the value is an object or array, JavaScript copies the reference, not the object itself.

Deep cloning is expensive — you'd need to recursively walk every nested structure, handle circular references, deal with special types like Dates, RegExps, Maps, Sets, and more. Spread keeps things fast and predictable by doing exactly one level. If you need a deep copy, use structuredClone(obj) which handles all those edge cases.

Quiz
What does merged.x equal?

Rest vs Spread: Same Syntax, Opposite Jobs

Both use ..., but they do fundamentally different things depending on context:

ContextNameWhat it Does
Left side of = (destructuring)RestCollects remaining elements into an array/object
Right side of = (expression)SpreadExpands elements out of an array/object
Function parameterRestCollects remaining arguments into an array
Function callSpreadExpands an array into separate arguments
// REST: collecting into a variable
const [first, ...rest] = [1, 2, 3, 4];
// first = 1, rest = [2, 3, 4]

// SPREAD: expanding into a new array
const newArr = [0, ...rest, 5];
// newArr = [0, 2, 3, 4, 5]

// REST: collecting function arguments
function sum(...numbers) {
  return numbers.reduce((total, n) => total + n, 0);
}

// SPREAD: expanding into function arguments
const vals = [1, 2, 3];
console.log(sum(...vals)); // 6

The simple rule: if ... appears where a value is being assigned to (left side, parameter), it's rest. If it appears where a value is being read from (right side, argument), it's spread.

Common Patterns in React

These features aren't just syntactic sugar — they're foundational to how React code is written.

Props Destructuring

// Instead of props.title, props.children everywhere
function Card({ title, children, className = '' }) {
  return (
    <div className={className}>
      <h2>{title}</h2>
      {children}
    </div>
  );
}

Forwarding Props

Pass known props and forward the rest to a child element:

function Button({ variant, size, children, ...rest }) {
  return (
    <button className={`btn btn-${variant} btn-${size}`} {...rest}>
      {children}
    </button>
  );
}

// onClick, disabled, aria-label, etc. all forwarded to <button>
<Button variant="primary" size="lg" onClick={handleClick} disabled>
  Submit
</Button>

Immutable State Updates

React state must be updated immutably — you can't mutate existing state. Spread makes this natural:

const [user, setUser] = useState({ name: 'Sam', age: 25, role: 'viewer' });

// Update one property without mutating
setUser({ ...user, age: 26 });

// Update nested state (need to spread each level)
const [state, setState] = useState({
  user: { name: 'Sam', preferences: { theme: 'dark' } }
});

setState({
  ...state,
  user: {
    ...state.user,
    preferences: {
      ...state.user.preferences,
      theme: 'light'
    }
  }
});

Adding and Removing from Arrays in State

const [items, setItems] = useState(['a', 'b', 'c']);

// Add to end
setItems([...items, 'd']);

// Add to beginning
setItems(['z', ...items]);

// Remove by filter (no mutation)
setItems(items.filter(item => item !== 'b'));
Quiz
What is wrong with this React state update?
What developers doWhat they should do
Expecting spread to deep clone nested objects
Spread copies property values — for objects/arrays, the value IS the reference, not a clone of the data.
Spread only copies one level. Nested objects are still shared references. Use structuredClone for deep copies.
Putting the rest element anywhere except last position
JavaScript needs to know where 'the rest' starts, which only works if it goes to the end.
The rest element (...rest) must always be the final element in destructuring.
Destructuring function parameters without a default empty object
Without = {}, calling the function with no arguments throws TypeError because you cannot destructure undefined.
Always add = {} when destructuring parameters: function fn({ a, b } = {})
Using spread order backwards for defaults: { ...userSettings, ...defaults }
Later properties override earlier ones. If defaults come last, they overwrite everything the user set.
Put defaults first: { ...defaults, ...userSettings } so user values override defaults.
Key Rules
  1. 1Array destructuring matches by position. Object destructuring matches by property name.
  2. 2Default values only activate when the value is undefined — not null, 0, or empty string.
  3. 3The rest element (...rest) must always be last and collects everything that wasn't explicitly destructured.
  4. 4Spread creates shallow copies only. Nested objects and arrays are shared references, not clones.
  5. 5In React, always use spread for immutable state updates: setUser({ ...user, age: 26 }) instead of mutating directly.