I love digital media - blogs, e-books, the more the better. But as a self-identifying bibliophile, I never go long without picking up an old-school, paper and ink book. When I want to deeply learn something I always come back to the printed word.
There's something about highlighting passages, and scribbling in the margins that cements information in my mind. I'm not alone in this, and there have been several studies that suggest students of all ages learn more, and retain more of what they learn, from physical books.
I'm currently working my way through the third edition of Eloquent JavaScript by Marijn Haverbeke. Though I consider JavaScript my primary language and felt comfortable with it before starting, I've learned something new, or gained a deeper understanding of something I already knew with every chapter.
During my study time the other night I came to a section about the ES6 features we all know and love, specifically destructuring. The section was short, more of a summary than a deep dive, but it made me realize I needed to know more about this ES6 feature - so I added it to the All Points Bulletin Board.
Question: What is Destructuring Assignment?
Short Answer: Destructuring is a succinct way to pull values out of an object and attach them to local variables in a single statement. It can be used on arrays or objects, allowing us to provide aliases, default values, and even complex expressions to obtain or replace data.
The Long Answer
The first time I saw destructuring was when learning React, and you'll see it in a lot of similar frameworks and libraries. It allows us to easily unpack multiple values from an array or object and assign them to variables in a single line.
Despite its name, destructuring isn't destructive - nothing is being destroyed. The original array is not mutated. Destructuring assignment allows us to literally deconstruct the object into its constituent parts, make copies and assign them to new local variables.
The feature came about because we had ways to construct multiple properties at the same time through object and array literals, but no comparable mechanism of extracting data - other than piecemeal assignments.
const numbersArray = [1, 2];
const x = numbersArray[0];
const y = numbersArray[1];
console.log(x, y);
//---> 1, 2
Destructuring assignment works on the 2 most used data structures in JavaScript - arrays and objects. Array destructuring and object destructuring are very similar, with a few notable differences. We'll talk about array destructuring first.
Array Destructuring
At first glance destructuring looks a lot like an array or object literal - but flipped. Instead of a variable declaration on the left with the assignment on the right - the extracted values appear to the left, and the sourced variable on the right.
const numbersArray = [1, 2]; // array literal
const [ x, y ] = numbersArray; // destructuring assignment
console.log(x, y);
//---> [1, 2]
Note: I've used a subtle naming convention in the code above. When I use destructuring I pad the variables in the destructuring array between brackets or braces with a leading and trailing space. This helps me read my code later, scanning files I can easily identify when I'm using destructuring assignment.
Arrays are obsessed with position, just take a look at their built-in methods, and how those methods traverse elements. Array destructuring is no different, as we saw in the example above. The variables we created were assigned their values after being mapped to the value at the same index in the sourced array.
Using that syntax we know how to grab values from an array, but what else can we do?
- We can assign values after declaration
let [a, b];
[ a, b ] = ["dog", "cat"];
- We can skip values
If the source variable contains values of no interest, they can be skipped with a comma and an empty space.
const dogs = ["Sparkles", "Rover", "Mosby", "Rufus"];
const [ a, , b, c ] = dogs;
const [ , , ,favorite ] = dogs;
console.log(a, b, c);
//---> "Sparkles", "Mosby", "Rufus"
console.log(favorite);
//---> "Rufus"
- We can manipulate values with array methods
We can chain other methods that also return an array - like .split
.
const name = "Mark Johnson";
const [ firstName, lastName ] = name.split(' ');
console.log(firstName);
//---> "Mark"
- We can provide default values
What if we try to unpack more values than the sourced array contains? In that case those empty variables will return undefined, and no error will be thrown.
To avoid ambiguity, we can provide a default value using the assignment operator.
const employeeHours = [34, 40];
const [ employeeOne = 30, employeeTwo = 30, employeeThree = 30 ] = employeeHours;
console.log(employeeThree);
//---> 30
console.log(employeeOne);
//---> 34
These default values can take on much more depth, becoming complex expressions or function calls. They will only be evaluated if a value cannot be found.
Below I've used the .reduce
method in a helper function to find the average of employeeHours
and assigned it as a back-up for employeeThree.
const employeeHours = [34, 40];
const findAvg = (hours) => hours.reduce((a, b) => a + b, 0) / hours.length;
const [ employeeOne, employeeTwo, employeeThree = findAvg(employeeHours) ] = employeeHours;
console.log(employeeThree);
//---> 37
- We can assign the Rest
If we extract only one value from the sourced iterable, then we only get that single value. What if we wanted to directly grab one or two values, but make sure the rest are still captured?
We can use 2 other ES6 features - the rest parameter and spread syntax. Using spread syntax (...
) before a variable name creates the rest parameter. Sometimes you'll hear this referred to as the "rest pattern".
Using the rest pattern is like selectively placing a couple of values in special boxes that can contain only one thing, and throwing the rest into a larger, catch-all box in case we need them later on.
const numbers = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10];
const [ first, second, ...rest ] = numbers;
console.log(rest);
//---> [3, 4, 5, 6, 7, 8, 9, 10]
const colors = ["red", "blue", "green", "yellow", "purple", "orangered", "goldenrod"];
const [ primary, secondary, accent, ...others ] = colors;
console.log(others);
//---> ["green", "yellow", "purple", "orangered", "goldenrod"]
Any name can be given to the rest parameter, but it must be proceeded by the spread syntax (...) and it must be the last variable in the destructuring array. If the rest parameter has a trailing comma it will throw an error.
- We can extract values from nested arrays
So far we've been working with single layer arrays, but destructuring works on nested arrays too. As always, position is important and the corresponding item must be an array.
const styles = [["desktop", [800, 100, "relative"]], ["mobile", [500, 80, "absolute"]]];
const [ [ className, [ height, width, position ] ] ] = styles;
console.log(className);
//---> "desktop"
console.log(height);
//---> 800
Now that we've seen the basics, let's look a couple of use cases for array destructuring.
Use Case 1: Working with returned arrays
Functions commonly return an array. Using array destructuring can make working with returned arrays DRY-er and easier to read.
In the function below, we return an array and assign their values using destructuring. You can see that we can skip values, assign the rest and more, just as before.
function dummy() {
return [10, 50, 30, 50, 60];
}
const [ first, second, , ...rest ] = dummy();
console.log(first);
//---> 10
console.log(rest);
//---> [50, 60]
Use Case 2: Swapping values
One very handy use case of destructuring is swapping the contents of two variables. Before ES6 this required creating a temporary variable.
let first = "one"
let second = "two"
// temporary variable
let originalFirst = first;
// swap the values
first = second;
second = originalFirst;
console.log(first, second)
//---> "two" "one"
First a new variable, originalFirst
, is created to capture the value of first
, ("one"). Then first
is directly reassigned to point to the value of second
, ("two"). Finally second
will be pointed to the value we captured in the temporary variable. If we didn't do this the original value of first
would be lost upon reassignment.
Using destructuring removes the need for a temporary local variable, and several lines of code altogether. In the destructuring array we'll declare the variables, and then swap them in the assignment, effectively trading values.
let first = "one"
let second = "two"
[ first, second ] = [second, first];
console.log(first, second);
//---> "two" "one"
Use Case 3: Regex patterns!
Regular expressions, regex for short, provide us a way to search a string against a pattern. Using the exec
function returns an array where the first element is the entire match, and the following elements are the smaller captured matches.
To illustrate what I mean, we'll look at a common regex pattern - the date matcher.
// this is a regex pattern - and a handy one to know!
const datePattern = /^([a-z]+)\s+(\d+)\s*,\s+(\d{4})$/i;
let [ fullDate, month, day, year ] = datePattern.exec("December 20, 2020") || [];
console.log(fullDate, month, day, year);
//---> "December 20, 2020" "December" "20" "2020"
The exec
function takes in the pattern to be searched for, and the string to be searched. It returns an array, containing first the full match with all of its spaces and characters ("December 20, 2020"), and then the individual matches that were captured ("December" "20" "2020").
Note that after calling the exec
function and passing in a string, we provided the logical ||
(or) operator and defaulted the return to be an empty array if no match could be found.
Though this post isn't really about regular expressions, they're an incredibly valuable tool to have in your belt. They can be used to search for word, date, and other patterns in text - capable of being as broad or specific as needed. Read more about regular expressions here.
Object Destructuring
Object destructuring is very similar to array destructuring, so we'll touch on it quickly, concentrating on the differences. We can extract properties much the same way we do with arrays, but instead of brackets, we use curly braces.
const dogs = {good: "Rover", gooder: "Sparkles", goodest: "Ace"};
const { good, gooder, goodest } = dogs;
console.log(good, gooder, goodest);
//---> "Rover" "Sparkles" "Ace"
If arrays are obsessed with position, objects are obsessed with name. That being the case, the property name must be spelled correctly with casing in mind, or it will return undefined.
const dogs = {good: "Rover", gooder: "Sparkles", goodest: "Ace"};
const { good, Gooder, goodest } = dogs;
console.log(good, Gooder, goodest);
//---> "Rover" undefined "Ace"
If we're not interested in a value we don't need to intentionally skip it. Instead, we can simply omit the property name. Position doesn't matter, we need only to be aware of the property names we're interested in, and their location relative to depth.
const dogs = {good: "Rover", gooder: "Sparkles", goodest: "Ace"};
const { good, goodest } = dogs;
console.log(good, goodest);
//---> "Rover" "Ace"
- We can assign values after declaration - with one gotcha
If we assign a value to a variable without a declaration, we must wrap the assignment statement in parentheses.
let first, second;
({ first, second } = {first: 1, second: 2}); // assignment statement
console.log(first, second);
//---> 1 2
If these parentheses are left out the statement will be read as a block, instead of an object literal destructuring assignment. They are not required when the declaration is present.
- We can provide default values and assign the Rest
We can capture the rest, and assign a default value, the same way we do in array destructuring - the only difference is the curly brackets.
const topDogs = {
first: "Rover",
second: "Marshmallow",
third: "Kurt",
honorableMentions: {
costume: "Sparkles",
personality: "Buccaneer"
}
};
const { first, third, fourth = "Cocoa", ...rest } = topDogs;
console.log(first, third, fourth);
//---> "Rover" "Kurt" "Cocoa"
console.log(rest);
//---> Object {honorableMentions: Object {costume: "Sparkles", personality: "Buccaneer"}, second: "Kurt"}
- We can provide an alias
If we want our identifier to have a different name than the property, we can provide an alias. Call the property name as usual, followed by a colon and the desired name.
const topDogs = {
first: "Rover",
second: "Marshmallow",
third: "Kurt",
};
const { first: gold, second: silver, third: bronze } = topDogs;
console.log(gold, silver, bronze);
//---> "Rover" "Marshmallow" "Kurt"
- We can extract values from nested objects
The properties on the objects we've seen so far have contained primitive data types, but they can also contain complex structures, like another object. We can use destructuring to access values in these nested structures.
const topDogs = {
first: "Rover",
second: "Marshmellow",
third: "Kurt",
honorableMentions: {
costume: "Sparkles",
personality: "Buccaneer"
}
};
const { honorableMentions: { costume: bestDressed } } = topDogs;
console.log(bestDressed);
//---> "Sparkles"
I like to think of these statements as a map with legend. honorableMentions
is not an identifier, or a variable. If we try to log it and peak at its guts, we won't find anything.
It just lets the compiler know to look for a nested object with name honorableMentions
on the first level of the sourced object, reach into it and grab the value of the property with name costume
and copy the value found there into our identifier bestDressed
.
We can extract values an unlimited amount of levels down. We just need to keep track of how many levels deep we are (with the presence of brackets) and how many stops there are on the way (with property names).
Without destructuring we could accomplish the same result with dot or bracket notation.
const topDogs = {
first: "Rover",
second: "Marshmellow",
third: "Kurt",
honorableMentions: {
costume: "Sparkles",
personality: "Buccaneer"
}
};
console.log(topDogs.honorableMentions.costume);
//---> "Sparkles"
Use Case: Destructuring props
Working with the props system in React often involves working with large, complex objects. Here destructuring can really shine - making components not only easier to read, but easier to write.
In this contrived example, we're passing a card object to a card viewing component through props.
import React from "react";
import "./styles.css";
import CardViewer from './CardViewer';
const cardData = {
front: "What does padding refer to in CSS?",
back: "Padding refers to the space between the border of the element and the content of the element",
user_id: 1,
public: true,
active: true,
notes: ["if border-box sizing is used padding will not effect the size of an element", "padding 'pads the content'"]
};
export default function App() {
const card = cardData;
return (
<div className="App">
<CardViewer card={card} />
</div>
);
}
Without destructuring we have to repeatedly use the props
prefix, and any further prefix needed, to get to the correct data in the CardViewer
component.
import React, { useState } from "react";
const CardViewer = (props) => {
const [ flipped, setFlipped ] = useState(false);
const flipCard = (e) => {
e.preventDefault();
setFlipped(!flipped);
}
return(
<div onClick={flipCard}>
<h3> {flipped ?
`${props.card.back}` :
`${props.card.front}`}
</h3>
<div>
<p>Notes:</p>
<ul>{props.card.notes.map((note)=>{
return <li>{note}</li>
})} </ul>
</div>
</div>
)
}
export default CardViewer;
Using destructuring we can easily grab the values we need in the component parameters, then in the return need only include the card
prefix.
import React, { useState } from "react";
const CardViewer = ({ card }) => {
const [ flipped, setFlipped ] = useState(false);
const flipCard = (e) => {
e.preventDefault();
setFlipped(!flipped);
}
return(
<div onClick={flipCard}>
<h3> {flipped ?
`${card.back}` :
`${card.front}`}
</h3>
<div>
<p>Notes:</p>
<ul>{card.notes.map((note)=>{
return <li>{note}</li>
})} </ul>
</div>
</div>
)
}
export default CardViewer;
We can take it even further with nested destructuring - grabbing only the things we're truly interested in. The resulting return reads almost like a sentence.
import React, { useState } from "react";
const CardViewer = ({ card: { front, back, notes: [ ...notes ] } }) => {
const [ flipped, setFlipped ] = useState(false);
const flipCard = (e) => {
e.preventDefault();
setFlipped(!flipped);
}
return(
<div onClick={flipCard}>
<h3> {flipped ?
`${back}` :
`${front}`}
</h3>
<div>
<p>Notes:</p>
<ul>{notes.map((note)=>{
return <li>{note}</li>
})} </ul>
</div>
</div>
)
}
export default CardViewer;
Destructuring is another invaluable ES6 feature, allowing us to extract properties and values the same way we can construct them with object literals. Destructuring saves time, reduces confusion and can result in elegant, readable code.
The next time you're working with complex data structures, keep this feature in your back pocket and you might just save yourself some time and headaches.
Resources
Eloquent JavaScript - Marijn Haverbeke
The Babel Replit - Always a good place to toss some code in and learn what it does under the hood
Array Destructuring in ES6 - FreeCodeCamp
Destructuring Assignment - MDN Web Docs
ES6 Destructuring: The Complete Guide - Glad China, CodeBurst
Destructuring Assignment - Javascipt.info
Destructuring - exloringjs.com
Destructuring Regular Expression Matching - Marius Schulz
JavaScript Object Destructuring - Dmitri Pavlutin
Destructuring & Function Arguments - David Walsh
Destructuring Props in React - Lindsay Criswell, Medium
🦄 Thanks for reading!
Top comments (5)
Awesome! This was a great read and I found myself saying "aha..." and "ooh.. cool" quite a few times. Even though I don't use react that much I could easily extract my own use cases. Thanks!
Thank you! Getting to be part of one "aha" moment is a wonderful feeling
Do you use another framework? I've worked with React for a lot of the 'learning phase' of becoming a developer, but I'm interested in taking stock of other popular approaches to grow my knowledge.
Where I work now we do mostly angular/typescript stuff. Coming from a java backend role it's been quite a pleasant learning path but there are still plenty of gaps in areas like the one in your post :)
Great post! haven't seen such a in depth article on destructuring, definitely a good read and overall informative post.
Thanks for reading, and the feedback. I was concerned it might be a little too long, but wanted the material to feel complete 🙂