Removing Duplicates with Map In JavaScript
Updated on · 5 min read|Removing duplicates from an array is a common task in JavaScript development, and using the Set
constructor is a popular way to achieve this. However, while it works great for arrays of primitive values, it doesn't work as expected for arrays of arrays or objects. This is because Set
stores either primitive values or object references. As a result, all values in the array have distinct references (unless an object/array is declared externally and referenced multiple times within the array), resulting in the original array remaining unchanged when converted to Set
. In this article, we'll explore an alternative approach using the Map
data structure, which maintains key uniqueness, to solve this problem. We'll also walk through a real-world use case for Map
, demonstrating how it can be used to efficiently collect unique values from an array of objects in a React app.
Removing duplicates from an array with Set
Removing duplicate items from an array in JavaScript is a common practice, and one way to achieve this is by using the Set constructor. This involves wrapping an existing array in a Set
and then converting it back to an array.
javascriptconst arr = [1, 2, "a", "b", 2, "a", 3, 4]; const uniqueArr = [...new Set(arr)]; console.log(uniqueArr); // [1, 2, "a", "b", 3, 4]
javascriptconst arr = [1, 2, "a", "b", 2, "a", 3, 4]; const uniqueArr = [...new Set(arr)]; console.log(uniqueArr); // [1, 2, "a", "b", 3, 4]
It is worth noting that Set
uses same-value-zero equality to determine value equality. As a result, a NaN
value will be considered equal to another NaN
, despite the fact that NaN !== NaN
. This means that if the original array contains multiple instances of NaN
, only one of them will be kept when the array is transformed into a Set.
If you're interested in learning more about NaN
and its type comparison, you may find this article helpful: What Is the Type of NaN?.
This works great for arrays of primitive values, however, when applying the same approach to an array of arrays or objects, the result is quite disappointing:
javascriptconst arr = [[1, 2], { a: "b" }, { a: 2 }, { a: "b" }, [3, 4], [1, 2]]; const uniqueArr = [...new Set(arr)]; console.log(uniqueArr); // [[1, 2], {'a': 'b'}, {'a':2}, {'a':'b'}, [3, 4], [1, 2]]
javascriptconst arr = [[1, 2], { a: "b" }, { a: 2 }, { a: "b" }, [3, 4], [1, 2]]; const uniqueArr = [...new Set(arr)]; console.log(uniqueArr); // [[1, 2], {'a': 'b'}, {'a':2}, {'a':'b'}, [3, 4], [1, 2]]
This is because Set
stores primitive values or object references, and in our case, all values in the array have different references.
It's worth noting that this approach will work if a non-primitive element is declared externally and then referenced multiple times within the array, since all its occurrences in the array will point to the same element. However, such a use case is not that common.
javascriptconst obj = { a: "b" }; const tuple = [1, 2]; const arr = [tuple, obj, { a: 2 }, obj, [3, 4], tuple]; const uniqueArr = [...new Set(arr)]; console.log(uniqueArr); // [[1, 2], {a: "b"}, {a:2}, [3, 4]]
javascriptconst obj = { a: "b" }; const tuple = [1, 2]; const arr = [tuple, obj, { a: 2 }, obj, [3, 4], tuple]; const uniqueArr = [...new Set(arr)]; console.log(uniqueArr); // [[1, 2], {a: "b"}, {a:2}, [3, 4]]
Before the introduction of Set
in JavaScript, filtering out duplicates from an array was most often done using a combination of filter
and indexOf
methods.
javascriptconst arr = [1, 2, "a", "b", 2, "a", 3, 4]; const uniqueArr = arr.filter((element, index) => { return arr.indexOf(element) === index; }); console.log(uniqueArr); // [1, 2, "a", "b", 3, 4]
javascriptconst arr = [1, 2, "a", "b", 2, "a", 3, 4]; const uniqueArr = arr.filter((element, index) => { return arr.indexOf(element) === index; }); console.log(uniqueArr); // [1, 2, "a", "b", 3, 4]
However, this approach is inefficient because, for each element iterated by filter
, there is an additional iteration over the whole array by the indexOf
method.
Using Map to remove duplicates from an array of objects
A not-so-well-known fact is that the Map
data structure maintains key uniqueness, meaning that there can be no more than one key-value pair with the same key in a given Map
. While knowing this won't help us magically transform any array into an array of unique values, certain use cases can benefit from Map's key uniqueness.
To demonstrate this, let's consider a sample React app that displays a list of books and includes a dropdown menu to filter books by their authors.
jsxconst App = () => { const books = [ { id: 1, name: "In Search of Lost Time ", author: { name: "Marcel Proust", id: 1 }, }, { id: 2, name: "Ulysses", author: { name: "James Joyce", id: 2 } }, { id: 3, name: "Don Quixote", author: { name: "Miguel de Cervantes", id: 3 }, }, { id: 4, name: "Hamlet", author: { name: "William Shakespeare", id: 4 } }, { id: 5, name: "Romeo and Juliet", author: { name: "William Shakespeare", id: 4 }, }, { id: 6, name: "Dubliners", author: { name: "James Joyce", id: 2 } }, ]; const [selectedAuthorId, setSelectedAuthorId] = useState(null); const filteredBooks = () => { if (!selectedAuthorId) { return books; } return books.filter((book) => String(book.author.id) === selectedAuthorId); }; return ( <div className="books"> <select className="books__select" onChange={({ target }) => setSelectedAuthorId(target.value)} > {/* TODO Show author options */} </select> <ul className="books__list"> {filteredBooks().map((book) => ( <li className="books__item"> {book.name} by {book.author.name} </li> ))} </ul> </div> ); };
jsxconst App = () => { const books = [ { id: 1, name: "In Search of Lost Time ", author: { name: "Marcel Proust", id: 1 }, }, { id: 2, name: "Ulysses", author: { name: "James Joyce", id: 2 } }, { id: 3, name: "Don Quixote", author: { name: "Miguel de Cervantes", id: 3 }, }, { id: 4, name: "Hamlet", author: { name: "William Shakespeare", id: 4 } }, { id: 5, name: "Romeo and Juliet", author: { name: "William Shakespeare", id: 4 }, }, { id: 6, name: "Dubliners", author: { name: "James Joyce", id: 2 } }, ]; const [selectedAuthorId, setSelectedAuthorId] = useState(null); const filteredBooks = () => { if (!selectedAuthorId) { return books; } return books.filter((book) => String(book.author.id) === selectedAuthorId); }; return ( <div className="books"> <select className="books__select" onChange={({ target }) => setSelectedAuthorId(target.value)} > {/* TODO Show author options */} </select> <ul className="books__list"> {filteredBooks().map((book) => ( <li className="books__item"> {book.name} by {book.author.name} </li> ))} </ul> </div> ); };
For simplicity, the books
array is hardcoded here, although in a real-world app, the data will be probably fetched from an API.
Now that the app is almost complete, we just need to render the dropdown of authors to filter by. One approach would be to collect the id
and name
of each author from our list of books into a separate array and render it as options inside the select
. However, this list should contain unique authors, or else authors of more than one book will appear in the dropdown multiple times.
Because the author's data is contained inside an object, we can't just apply the Set
trick to get unique values only. One option would be to first get all the ids for the authors into an array, then apply Set
to it to get unique ones, and after that iterate over the authors
array once more to collect their names based on the ids. However, this sounds like a lot of work.
Another solution, similar to the way for filtering out duplicates from an array in JavaScript before the introduction of Set
, is to use a combination of filter
and findIndexOf
.
jsconst authorOptions = books .filter((book, index, array) => { const bookIndex = array.findIndex((b) => book.author.id === b.author.id); return index === bookIndex; }) .map((book) => ({ id: book.author.id, author: book.author.name }));
jsconst authorOptions = books .filter((book, index, array) => { const bookIndex = array.findIndex((b) => book.author.id === b.author.id); return index === bookIndex; }) .map((book) => ({ id: book.author.id, author: book.author.name }));
While this is better than the first approach, it is still quite verbose, and for our particular use case, we need to run an extra map
iteration over the result to transform it into the shape we need for the options. Luckily, there's an easier solution.
We can extract an array of id
-name
pairs from the books
list and transform it into a Map
. This approach automatically takes care of preserving only the pairs with unique keys, avoiding the need for multiple iterations and unnecessary code.
javascriptconst authorOptions = new Map([ ...books.map((book) => [book.author.id, book.author.name]), ]);
javascriptconst authorOptions = new Map([ ...books.map((book) => [book.author.id, book.author.name]), ]);
That's it! Now we have a Map
of unique key-value pairs that we can feed directly into our select
component. It's worth noting that when a Map
preserves key uniqueness, the last inserted item with the existing key remains in the Map
, while previous duplicates are discarded.
javascriptconst map1 = new Map([ [1, 3], [2, 3], ]); const map2 = new Map([[1, 2]]); const merged = new Map([...map1, ...map2]); console.log(merged.get(1)); // 2 console.log(merged.get(2)); // 3
javascriptconst map1 = new Map([ [1, 3], [2, 3], ]); const map2 = new Map([[1, 2]]); const merged = new Map([...map1, ...map2]); console.log(merged.get(1)); // 2 console.log(merged.get(2)); // 3
However, in our example app, all the author id
-name
pairs are unique, so we don't need to worry about accidentally overriding any data.
Now we can combine everything into the final version of the component.
jsxconst App = () => { const books = [ { id: 1, name: "In Search of Lost Time ", author: { name: "Marcel Proust", id: 1 }, }, { id: 2, name: "Ulysses", author: { name: "James Joyce", id: 2 } }, { id: 3, name: "Don Quixote", author: { name: "Miguel de Cervantes", id: 3 }, }, { id: 4, name: "Hamlet", author: { name: "William Shakespeare", id: 4 } }, { id: 5, name: "Romeo and Juliet", author: { name: "William Shakespeare", id: 4 }, }, { id: 6, name: "Dubliners", author: { name: "James Joyce", id: 2 } }, ]; const [selectedAuthorId, setSelectedAuthorId] = useState(null); const authorOptions = new Map([ ...books.map((book) => [book.author.id, book.author.name]), ]); const filteredBooks = () => { if (!selectedAuthorId) { return books; } return books.filter((book) => String(book.author.id) === selectedAuthorId); }; return ( <div className="books"> <select className="books__select" onChange={({ target }) => setSelectedAuthorId(target.value)} > <option value="">--Select author--</option> {[...authorOptions].map(([id, name]) => ( <option value={id}>{name}</option> ))} </select> <ul className="books__list"> {filteredBooks().map((book) => ( <li className="books__item"> {book.name} by {book.author.name} </li> ))} </ul> </div> ); };
jsxconst App = () => { const books = [ { id: 1, name: "In Search of Lost Time ", author: { name: "Marcel Proust", id: 1 }, }, { id: 2, name: "Ulysses", author: { name: "James Joyce", id: 2 } }, { id: 3, name: "Don Quixote", author: { name: "Miguel de Cervantes", id: 3 }, }, { id: 4, name: "Hamlet", author: { name: "William Shakespeare", id: 4 } }, { id: 5, name: "Romeo and Juliet", author: { name: "William Shakespeare", id: 4 }, }, { id: 6, name: "Dubliners", author: { name: "James Joyce", id: 2 } }, ]; const [selectedAuthorId, setSelectedAuthorId] = useState(null); const authorOptions = new Map([ ...books.map((book) => [book.author.id, book.author.name]), ]); const filteredBooks = () => { if (!selectedAuthorId) { return books; } return books.filter((book) => String(book.author.id) === selectedAuthorId); }; return ( <div className="books"> <select className="books__select" onChange={({ target }) => setSelectedAuthorId(target.value)} > <option value="">--Select author--</option> {[...authorOptions].map(([id, name]) => ( <option value={id}>{name}</option> ))} </select> <ul className="books__list"> {filteredBooks().map((book) => ( <li className="books__item"> {book.name} by {book.author.name} </li> ))} </ul> </div> ); };
If you are curious about other ways Map
can help you optimize your code,
I've written a separate article: Simplifying Code with Maps In JavaScript and
React.
Conclusion
In conclusion, removing duplicate values from an array is a common task in JavaScript development, and using the Set
constructor is a popular way to achieve this. However, it falls short when dealing with arrays of objects or arrays of arrays. In this post, we explored an alternative approach to remove duplicates from an array by using the Map
data structure. By harnessing the power of Map's key uniqueness, we have demonstrated a real-world use case in a React app, where we efficiently collected unique values from an array of objects.
Using the Map
approach allows for preserving only the unique key-value pairs, which is not possible with the Set
approach when dealing with arrays of non-primitive values. This method is particularly useful when working with arrays of objects, and when we need to filter out duplicates based on a particular key value.
We also explored other techniques for removing duplicates, such as using the filter
in combination with the indexOf
or findIndex
methods, but these can be less efficient and more verbose compared to using Map
.
Overall, whether you are dealing with arrays of primitives, objects, or arrays, it's good to know that there are multiple ways to remove duplicates and that choosing the right approach depends on your specific use case.