deep-diff, deep-object-diff, and object-diff are all npm packages designed to compute structural differences between JavaScript objects, including nested properties, arrays, and complex data types. These libraries help developers detect changes in state, synchronize data, or implement undo/redo functionality by identifying what has changed between two object snapshots. While they share a common goal, they differ significantly in API design, output format, handling of edge cases, and maintenance status.
When building frontend applications that track state changes — like form validation, real-time sync, or undo stacks — you often need to compare complex nested objects. Native equality checks (===) fail here because they only compare references. That’s where dedicated diffing libraries come in. Let’s examine how deep-diff, deep-object-diff, and object-diff approach this problem, and why one might be better suited for your use case.
First, a critical note: object-diff is deprecated. Its npm page states: "This package has been deprecated. Please use deep-object-diff instead." The GitHub repo is archived and read-only. It also has significant limitations:
{} even when differences exist in deeper levels// object-diff (deprecated — do not use)
import objectDiff from 'object-diff';
const a = { user: { name: 'Alice', age: 30 } };
const b = { user: { name: 'Alice', age: 31 } };
console.log(objectDiff(a, b)); // {} — fails to detect nested change!
Given these flaws and its unmaintained status, skip object-diff entirely. We’ll focus the rest of this comparison on the two viable options.
The biggest difference between deep-diff and deep-object-diff lies in what they return.
deep-diff gives you an array of change records, each describing the type of operation (N for new, D for deleted, E for edited, A for array), the path to the change, and old/new values.
// deep-diff
import diff from 'deep-diff';
const a = { user: { name: 'Alice', tags: ['dev'] } };
const b = { user: { name: 'Bob', tags: ['dev', 'frontend'] } };
const changes = diff(a, b);
console.log(changes);
// [
// { kind: 'E', path: ['user', 'name'], lhs: 'Alice', rhs: 'Bob' },
// { kind: 'A', path: ['user', 'tags'], index: 1, item: { kind: 'N', rhs: 'frontend' } }
// ]
This format is powerful when you need to interpret or replay changes — for example, to generate audit logs or apply patches later.
deep-object-diff, by contrast, returns a plain object showing only the paths and new values that differ. Deleted keys are represented with undefined.
// deep-object-diff
import diff from 'deep-object-diff';
const a = { user: { name: 'Alice', tags: ['dev'] } };
const b = { user: { name: 'Bob', tags: ['dev', 'frontend'] } };
const delta = diff(a, b);
console.log(delta);
// { user: { name: 'Bob', tags: ['dev', 'frontend'] } }
// For deletions:
const c = { name: 'Alice', role: 'admin' };
const d = { name: 'Alice' };
console.log(diff(c, d)); // { role: undefined }
This is simpler and integrates cleanly with patterns like Object.assign() or spread operators, making it great for lightweight state updates.
If you need to apply a diff back to an object (e.g., to revert a change), deep-diff includes a patch function:
// deep-diff: applying patches
import { diff, patch } from 'deep-diff';
const original = { count: 5 };
const updated = { count: 10 };
const changes = diff(original, updated);
const reverted = patch(updated, changes.map(change => ({
...change,
kind: change.kind === 'E' ? 'E' : change.kind, // invert logic as needed
// Note: full inversion requires custom logic per kind
})));
// deep-diff doesn't auto-invert, but provides tools to build patching
deep-object-diff has no built-in patching. You’d merge the delta manually:
// deep-object-diff: manual merge
import diff from 'deep-object-diff';
const base = { a: 1, b: 2 };
const delta = diff(base, { a: 3 });
const merged = { ...base, ...delta }; // { a: 3, b: 2 }
// But this fails for nested deletes — you'd need a recursive merge utility
So if patching or undo functionality is core to your app, deep-diff’s structured output gives you more control.
Both libraries handle nested objects and arrays, but differently.
Arrays:
deep-diff treats array modifications as special A (array) kind entries, showing index-level changes.deep-object-diff returns the entire new array if any element differs.// Array handling
const arrA = { items: [1, 2] };
const arrB = { items: [1, 3] };
// deep-diff
console.log(diff(arrA, arrB));
// [{ kind: 'A', path: ['items'], index: 1, item: { kind: 'E', lhs: 2, rhs: 3 } }]
// deep-object-diff
console.log(diff(arrA, arrB));
// { items: [1, 3] }
Dates and other objects:
deep-diff compares Date objects by value (via .getTime()), and handles RegExp, Buffer, etc.deep-object-diff compares non-plain objects by reference, so two new Date('2023') instances will appear different even if they represent the same time.Circular references:
deep-diff detects and safely skips circular structures.deep-object-diff may crash or recurse infinitely on circular objects (though recent versions attempt basic cycle detection).deep-object-diff follows a pure functional style: one function, one job, no side effects.
import diff from 'deep-object-diff';
const delta = diff(obj1, obj2);
deep-diff offers more methods (diff, patch, applyChange, revertChange) and optional configuration (like prefiltering properties), which adds flexibility at the cost of API surface area.
You want to avoid unnecessary re-renders by checking if state actually changed.
deep-object-diff{} (no change) or not. Simple and fast.const prev = store.getState();
const next = reducer(prev, action);
if (Object.keys(diff(prev, next)).length > 0) {
// dispatch update
}
You need to log exactly what changed in a user profile (e.g., “email changed from X to Y”).
deep-diffconst changes = diff(oldProfile, newProfile);
changes.forEach(change => {
if (change.kind === 'E') {
console.log(`${change.path.join('.')} changed from ${change.lhs} to ${change.rhs}`);
}
});
You send only changed fields to reduce payload size.
deep-object-diffconst local = { name: 'Alice', email: 'a@example.com' };
const server = { name: 'Alice', email: 'old@example.com' };
const patchBody = diff(server, local); // { email: 'a@example.com' }
fetch('/api/user', { method: 'PATCH', body: JSON.stringify(patchBody) });
| Feature | deep-diff | deep-object-diff | object-diff |
|---|---|---|---|
| Status | Unmaintained but functional | Actively maintained | ❌ Deprecated |
| Output | Array of change records | Plain delta object | Broken for nested objects |
| Patch Support | ✅ Built-in utilities | ❌ Manual merge only | ❌ |
| Array Diffing | ✅ Index-level changes | ❌ Full array replacement | ❌ |
| Non-Plain Objects | ✅ Handles Date, RegExp, etc. | ⚠️ Reference comparison only | ❌ |
| Circular References | ✅ Safe handling | ⚠️ May fail | ❌ |
| Use Case Fit | Audit logs, undo/redo, patching | Lightweight change detection | Do not use |
deep-diff, but test thoroughly in your environment.deep-object-diff is the modern, reliable choice.object-diff — it’s deprecated and fundamentally broken for real-world data.In most contemporary frontend applications, deep-object-diff strikes the best balance between simplicity, correctness, and maintainability. Reserve deep-diff for specialized scenarios where its rich metadata justifies the added complexity.
Choose deep-object-diff if you prefer a minimal, functional approach that returns a plain object representing only the differing paths and values, without metadata about change types. It’s ideal for simple change detection where you just need to know ‘what’s different’ rather than ‘how it changed,’ and works well in Redux-style reducers or lightweight reactivity systems. Its zero-dependency, immutable design makes it easy to reason about and integrate.
Choose deep-diff if you need a mature, feature-rich library that supports detailed change tracking (including kind of change like 'add', 'delete', 'edit', or 'array'), handles circular references, and provides utilities to apply patches. It’s well-suited for applications requiring precise diff interpretation and mutation replay, such as collaborative editing or state history systems. However, note that it hasn’t seen active development recently, so evaluate its compatibility with modern environments carefully.
Avoid object-diff in new projects — it is officially deprecated on npm and its GitHub repository is archived. The package lacks support for arrays, functions, dates, and other non-plain objects, and offers no meaningful advantages over maintained alternatives. Use deep-object-diff or deep-diff instead depending on your needs.
❄️
Deep diff two JavaScript Objects
A small library that can deep diff two JavaScript Objects, including nested structures of arrays and objects.
yarn add deep-object-diff
npm i --save deep-object-diff
diff(originalObj, updatedObj)
returns the difference of the original and updated objects
addedDiff(original, updatedObj)
returns only the values added to the updated object
deletedDiff(original, updatedObj)
returns only the values deleted in the updated object
updatedDiff(original, updatedObj)
returns only the values that have been changed in the updated object
detailedDiff(original, updatedObj)
returns an object with the added, deleted and updated differences
import { diff, addedDiff, deletedDiff, updatedDiff, detailedDiff } from 'deep-object-diff';
diff:const lhs = {
foo: {
bar: {
a: ['a', 'b'],
b: 2,
c: ['x', 'y'],
e: 100 // deleted
}
},
buzz: 'world'
};
const rhs = {
foo: {
bar: {
a: ['a'], // index 1 ('b') deleted
b: 2, // unchanged
c: ['x', 'y', 'z'], // 'z' added
d: 'Hello, world!' // added
}
},
buzz: 'fizz' // updated
};
console.log(diff(lhs, rhs)); // =>
/*
{
foo: {
bar: {
a: {
'1': undefined
},
c: {
'2': 'z'
},
d: 'Hello, world!',
e: undefined
}
},
buzz: 'fizz'
}
*/
addedDiff:const lhs = {
foo: {
bar: {
a: ['a', 'b'],
b: 2,
c: ['x', 'y'],
e: 100 // deleted
}
},
buzz: 'world'
};
const rhs = {
foo: {
bar: {
a: ['a'], // index 1 ('b') deleted
b: 2, // unchanged
c: ['x', 'y', 'z'], // 'z' added
d: 'Hello, world!' // added
}
},
buzz: 'fizz' // updated
};
console.log(addedDiff(lhs, rhs));
/*
{
foo: {
bar: {
c: {
'2': 'z'
},
d: 'Hello, world!'
}
}
}
*/
deletedDiff:const lhs = {
foo: {
bar: {
a: ['a', 'b'],
b: 2,
c: ['x', 'y'],
e: 100 // deleted
}
},
buzz: 'world'
};
const rhs = {
foo: {
bar: {
a: ['a'], // index 1 ('b') deleted
b: 2, // unchanged
c: ['x', 'y', 'z'], // 'z' added
d: 'Hello, world!' // added
}
},
buzz: 'fizz' // updated
};
console.log(deletedDiff(lhs, rhs));
/*
{
foo: {
bar: {
a: {
'1': undefined
},
e: undefined
}
}
}
*/
updatedDiff:const lhs = {
foo: {
bar: {
a: ['a', 'b'],
b: 2,
c: ['x', 'y'],
e: 100 // deleted
}
},
buzz: 'world'
};
const rhs = {
foo: {
bar: {
a: ['a'], // index 1 ('b') deleted
b: 2, // unchanged
c: ['x', 'y', 'z'], // 'z' added
d: 'Hello, world!' // added
}
},
buzz: 'fizz' // updated
};
console.log(updatedDiff(lhs, rhs));
/*
{
buzz: 'fizz'
}
*/
detailedDiff:const lhs = {
foo: {
bar: {
a: ['a', 'b'],
b: 2,
c: ['x', 'y'],
e: 100 // deleted
}
},
buzz: 'world'
};
const rhs = {
foo: {
bar: {
a: ['a'], // index 1 ('b') deleted
b: 2, // unchanged
c: ['x', 'y', 'z'], // 'z' added
d: 'Hello, world!' // added
}
},
buzz: 'fizz' // updated
};
console.log(detailedDiff(lhs, rhs));
/*
{
added: {
foo: {
bar: {
c: {
'2': 'z'
},
d: 'Hello, world!'
}
}
},
deleted: {
foo: {
bar: {
a: {
'1': undefined
},
e: undefined
}
}
},
updated: {
buzz: 'fizz'
}
}
*/
MIT