Lodash xorWith with Complex Object Arrays

This article explains how the Lodash utility library’s _.xorWith function handles symmetric differences between arrays of complex objects. You will learn how _.xorWith operates under the hood, how it overcomes the limitations of reference-based comparisons using custom comparator functions, and how to implement it effectively using deep equality and custom property matching.

In JavaScript, objects are compared by reference rather than by value. Standard difference methods like Lodash's _.xor use SameValueZero equality, meaning two separate objects with identical keys and values are considered distinct because they point to different memory addresses. _.xorWith solves this problem by accepting an array of target arrays followed by a comparator function that defines what makes two elements equal.

The function computes the symmetric difference (the values that are present in one array or the other, but not both) by iteratively comparing elements across the provided arrays using the comparator. If the comparator evaluates to true for a pair of items, both are treated as matching and excluded from the resulting array. Any object that does not find a match based on the comparator is preserved.

Basic Syntax and Parameters

_.xorWith([arrays], [comparator])

Deep Equality with _.isEqual

When working with complex objects that contain nested properties, the most common approach is pairing _.xorWith with Lodash’s deep comparison method, _.isEqual.

const _ = require('lodash');

const listA = [
  { id: 1, profile: { name: 'Alice', role: 'Admin' } },
  { id: 2, profile: { name: 'Bob', role: 'User' } }
];

const listB = [
  { id: 2, profile: { name: 'Bob', role: 'User' } },
  { id: 3, profile: { name: 'Charlie', role: 'User' } }
];

const result = _.xorWith(listA, listB, _.isEqual);

In this scenario, result contains:

[
  { id: 1, profile: { name: 'Alice', role: 'Admin' } },
  { id: 3, profile: { name: 'Charlie', role: 'User' } }
]

The object representing "Bob" is present in both arrays with identical nested properties. _.isEqual evaluates the values deeply, identifies them as equivalent, and _.xorWith filters them out.

Custom Property Comparators

You do not need to evaluate the entire object structure if only specific attributes determine uniqueness. You can provide an inline function to compare specific keys, such as unique identifiers:

const currentUsers = [
  { id: 101, status: 'active', lastLogin: '2023-01-01' },
  { id: 102, status: 'pending', lastLogin: '2023-02-01' }
];

const updatedUsers = [
  { id: 101, status: 'active', lastLogin: '2023-05-15' },
  { id: 103, status: 'active', lastLogin: '2023-05-20' }
];

// Compare only by the 'id' field
const idDifference = _.xorWith(currentUsers, updatedUsers, (a, b) => a.id === b.id);

Even though the object with id: 101 has differing lastLogin values, the custom comparator treats them as identical based on a.id === b.id, excluding it from the output. Only the objects with IDs 102 and 103 are returned.

Performance Considerations

_.xorWith operates with an approximate time complexity of \(O(n \times m)\), where \(n\) and \(m\) are the lengths of the arrays being compared, as it evaluates candidate pairs against the comparator. When dealing with deep equality checks via _.isEqual on massive datasets or deeply nested structures, this can introduce performance overhead. In high-throughput environments with large datasets, mapping objects to unique primitive keys (such as an ID string) using a Map or Set may provide faster alternatives. For typical application state management and data synchronization, _.xorWith provides a clean, declarative solution for complex array operations.