How to Remove Duplicate Objects from an Array in JavaScript: Best Methods Explained
Working with arrays of objects is a common task in JavaScript, whether you’re handling API responses, user input, or data aggregation. A frequent challenge is dealing with duplicate objects—multiple objects that represent the same data but exist as separate references in memory. Unlike primitive values (e.g., strings, numbers), objects are compared by reference, not value, so { id: 1 } === { id: 1 } returns false even if their content is identical.
This blog will guide you through proven methods to remove duplicate objects from an array, explaining when to use each approach, their pros and cons, and providing actionable code examples. By the end, you’ll be equipped to choose the best method for your specific use case.
Table of Contents#
- Understanding the Problem: Why Objects Are Tricky
- Method 1: Using
Setwith a Unique Key - Method 2: Using
Array.reduce() - Method 3: Using
Array.filter()withfindIndex - Method 4: Using Lodash’s
uniqBy - Method 5: Using
Mapfor Key-Value Tracking - Method 6: Deep Comparison (No Unique Key)
- Comparison of Methods: When to Use Which?
- Conclusion
- References
Understanding the Problem: Why Objects Are Tricky#
In JavaScript, primitives (e.g., 'a', 42) are compared by value, so duplicates in an array of primitives can be easily removed with a Set:
const primitiveArray = [1, 2, 2, 3, 3, 3];
const uniquePrimitives = [...new Set(primitiveArray)]; // [1, 2, 3]Objects, however, are reference types. Two objects with identical properties are stored at different memory addresses, so === returns false:
const obj1 = { id: 1 };
const obj2 = { id: 1 };
console.log(obj1 === obj2); // false (different references)Thus, removing duplicates requires comparing object content, not references. The solution depends on whether your objects have a unique identifier (e.g., id) or require deep comparison (checking all properties).
Method 1: Using Set with a Unique Key#
How It Works#
If your objects have a unique identifier (e.g., id, email), you can track duplicates by storing these keys in a Set. A Set automatically handles uniqueness, so you can filter the array to include only objects whose keys haven’t been seen before.
Code Example#
Suppose we have an array of user objects with a unique id:
const users = [
{ id: 1, name: "Alice", email: "[email protected]" },
{ id: 2, name: "Bob", email: "[email protected]" },
{ id: 1, name: "Alice", email: "[email protected]" }, // Duplicate (same id)
{ id: 3, name: "Charlie", email: "[email protected]" },
{ id: 2, name: "Bob", email: "[email protected]" }, // Duplicate (same id)
];
// Step 1: Track seen unique keys (e.g., `id`) in a Set
const seenIds = new Set();
// Step 2: Filter the array to keep only objects with unseen keys
const uniqueUsers = users.filter(user => {
if (!seenIds.has(user.id)) {
seenIds.add(user.id);
return true; // Keep the object
}
return false; // Skip duplicates
});
console.log(uniqueUsers);
// Output: [{ id: 1, ... }, { id: 2, ... }, { id: 3, ... }] (no duplicates)Pros#
- Efficient:
Setoperations (has,add) are O(1), so the overall time complexity is O(n). - Simple: Easy to read and implement.
Cons#
- Requires a Unique Key: Only works if objects have a distinct identifier (e.g.,
id). - Key Type Sensitivity: If keys are of mixed types (e.g.,
1vs"1"), they’ll be treated as unique.
Method 2: Using Array.reduce()#
How It Works#
The reduce method iterates over the array and builds a new array of unique objects. We use an accumulator (an object or Set) to track seen keys, adding objects to the result only if their key is new.
Code Example#
Using the same users array with id as the unique key:
const uniqueUsers = users.reduce((acc, user) => {
// Check if the user's id is already in the accumulator's `seenIds` Set
const seenIds = acc.seenIds;
if (!seenIds.has(user.id)) {
seenIds.add(user.id);
acc.result.push(user); // Add to result if id is new
}
return acc;
}, { seenIds: new Set(), result: [] }).result; // Initial accumulator
console.log(uniqueUsers);
// Output: [{ id: 1, ... }, { id: 2, ... }, { id: 3, ... }]Pros#
- Flexible: Can track multiple keys (e.g.,
id+email) by combining them into a composite key. - No External Dependencies: Uses native JavaScript methods.
Cons#
- More Verbose: Slightly longer than the
Set+filterapproach.
Method 3: Using Array.filter() with findIndex#
How It Works#
Array.filter() creates a new array with objects that pass a test. For each object, we check if its first occurrence in the array (via findIndex) is the same as its current index. If findIndex returns the current index, the object is unique; otherwise, it’s a duplicate.
Code Example#
Using id as the unique key:
const uniqueUsers = users.filter((user, index) => {
// Find the first index where the `id` matches the current user's id
const firstOccurrenceIndex = users.findIndex(u => u.id === user.id);
// Keep the object only if it's the first occurrence
return firstOccurrenceIndex === index;
});
console.log(uniqueUsers);
// Output: [{ id: 1, ... }, { id: 2, ... }, { id: 3, ... }]Pros#
- Intuitive: Directly checks for the first occurrence of an object.
Cons#
- Inefficient for Large Arrays:
findIndexiterates the array for every object, leading to O(n²) time complexity. Avoid for arrays with 1000+ items.
Method 4: Using Lodash’s uniqBy#
How It Works#
Lodash, a popular utility library, provides _.uniqBy, which removes duplicates by a specified key. It’s concise and handles edge cases (e.g., null/undefined keys) gracefully.
Code Example#
First, install Lodash (npm install lodash), then import uniqBy:
import { uniqBy } from "lodash";
const uniqueUsers = uniqBy(users, "id"); // Deduplicate by `id`
console.log(uniqueUsers);
// Output: [{ id: 1, ... }, { id: 2, ... }, { id: 3, ... }]Pros#
- Concise: One line of code.
- Battle-Tested: Handles edge cases (e.g., nested keys with
_.uniqBy(users, u => u.profile.id)).
Cons#
- Adds a Dependency: Increases bundle size (Lodash core is ~70KB minified). Use
lodash-eswith tree-shaking to reduce size.
Method 5: Using Map for Key-Value Tracking#
How It Works#
A Map object stores key-value pairs and remembers the insertion order. Like Set, it can track unique keys, but it also allows you to store the entire object for quick access.
Code Example#
const userMap = new Map();
// Use `id` as the key; if the key doesn't exist, add the object to the Map
users.forEach(user => {
if (!userMap.has(user.id)) {
userMap.set(user.id, user);
}
});
// Convert Map values back to an array
const uniqueUsers = Array.from(userMap.values());
console.log(uniqueUsers);
// Output: [{ id: 1, ... }, { id: 2, ... }, { id: 3, ... }]Pros#
- Readable: Explicitly maps keys to objects, making intent clear.
- Order-Preserving: Maintains the order of first occurrences (unlike older
Setimplementations in some browsers).
Cons#
- Slightly Overkill: For simple cases,
Set+filteris more concise.
Method 6: Deep Comparison (No Unique Key)#
When to Use This#
If your objects lack a unique key (e.g., { name: "Alice" } and { name: "Alice" }), you need to compare all properties. This is called "deep comparison."
Approach 1: JSON.stringify + Set#
Convert objects to JSON strings and use a Set to track duplicates. This works for simple objects with serializable values (strings, numbers, arrays).
Code Example#
const objects = [
{ name: "Alice", age: 30 },
{ age: 30, name: "Alice" }, // Same properties, different order
{ name: "Bob", age: 25 },
{ name: "Alice", age: 30 }, // Duplicate
];
// Sort object keys to handle inconsistent property order
const stringifyWithSortedKeys = (obj) =>
JSON.stringify(Object.entries(obj).sort());
const seenStrings = new Set();
const uniqueObjects = objects.filter(obj => {
const str = stringifyWithSortedKeys(obj);
if (!seenStrings.has(str)) {
seenStrings.add(str);
return true;
}
return false;
});
console.log(uniqueObjects);
// Output: [{ name: "Alice", age: 30 }, { name: "Bob", age: 25 }]Approach 2: Custom Deep Compare Function#
For complex objects (e.g., with nested objects, Date, or RegExp), JSON.stringify may fail (e.g., Date objects serialize to strings, but new Date(2023) and new Date("2023-01-01") are different strings). A custom deep compare function is more reliable but complex.
Code Example (Simplified Deep Compare)#
const isDeepEqual = (obj1, obj2) => {
// Handle primitive values and references
if (obj1 === obj2) return true;
// Check if both are objects and not null
if (typeof obj1 !== "object" || obj1 === null || typeof obj2 !== "object" || obj2 === null) {
return false;
}
// Compare keys
const keys1 = Object.keys(obj1);
const keys2 = Object.keys(obj2);
if (keys1.length !== keys2.length) return false;
// Recursively compare values
for (const key of keys1) {
if (!isDeepEqual(obj1[key], obj2[key])) return false;
}
return true;
};
// Filter duplicates using deep comparison
const uniqueObjects = objects.filter((obj, index) => {
return objects.findIndex(otherObj => isDeepEqual(obj, otherObj)) === index;
});Pros#
- No Unique Key Needed: Compares object content directly.
Cons#
- Performance:
JSON.stringifyand custom deep compare are O(n²) for large arrays. - Limitations:
JSON.stringifyfails for non-serializable values (e.g.,Symbol,Function,undefined).
Comparison of Methods: When to Use Which?#
| Method | Use Case | Time Complexity | Pros | Cons |
|---|---|---|---|---|
Set + filter | Objects with a unique key (e.g., id) | O(n) | Fast, simple | Requires a unique key |
reduce | Objects with a unique key | O(n) | Flexible (supports composite keys) | Slightly verbose |
filter + findIndex | Small arrays with a unique key | O(n²) | Intuitive | Slow for large arrays |
Lodash uniqBy | Projects using Lodash | O(n) | Concise, handles edge cases | Adds dependency |
Map | Preserving insertion order | O(n) | Readable, order-preserving | Overkill for simple cases |
| Deep Comparison | No unique key, simple objects | O(n²) | Compares full content | Slow, limited by serialization |
Conclusion#
Removing duplicate objects in JavaScript depends on your data structure and needs:
- Use
Set+filterorreduceif you have a unique key (fast and native). - Use Lodash’s
uniqByfor concise code in Lodash projects. - Use deep comparison only when no unique key exists (e.g., ad-hoc objects).
Always prioritize methods with O(n) time complexity for large datasets, and test edge cases like missing keys or non-serializable values.