Shallow vs Deep Equality
Compare references vs structure — React prop checks, writing shallowEqual, deep equal costs, and JSON.stringify traps.
- javascript
- shallow-vs
“Are these the same?” needs a definition. Reference equality (===) asks if two bindings point to the same object. Shallow equality compares one level of properties. Deep equality walks the whole graph. Pick wrong and you skip re-renders or thrash them.
Reference
const a = { x: 1 };
const b = { x: 1 };
a === b; // false
a === a; // true
Cheap and correct for “same instance.” Wrong for “same data from an API.”
Shallow
function shallowEqual(a, b) {
if (Object.is(a, b)) return true;
if (typeof a !== 'object' || a === null || typeof b !== 'object' || b === null) {
return false;
}
const ka = Object.keys(a);
const kb = Object.keys(b);
if (ka.length !== kb.length) return false;
for (const k of ka) {
if (!Object.hasOwn(b, k) || !Object.is(a[k], b[k])) return false;
}
return true;
}
shallowEqual({ x: 1 }, { x: 1 }); // true
shallowEqual({ n: { v: 1 } }, { n: { v: 1 } }); // false — nested refs differ
React.memo / PureComponent default to shallow compare props/state.
Deep
// illustrative — production: use a tested lib if you must
function deepEqual(a, b, seen = new WeakMap()) {
if (Object.is(a, b)) return true;
if (typeof a !== typeof b) return false;
if (typeof a !== 'object' || a === null || b === null) return false;
if (seen.get(a) === b) return true;
seen.set(a, b);
if (Array.isArray(a) !== Array.isArray(b)) return false;
const ka = Object.keys(a);
const kb = Object.keys(b);
if (ka.length !== kb.length) return false;
for (const k of ka) {
if (!Object.hasOwn(b, k) || !deepEqual(a[k], b[k], seen)) return false;
}
return true;
}
Costs grow with structure size. Cycles need a seen map. Dates, Maps, Sets, RegExps need special cases — JSON won’t save you.
JSON.stringify equality — brittle
JSON.stringify({ b: 1, a: 2 }) === JSON.stringify({ a: 2, b: 1 }); // false — key order
JSON.stringify({ a: undefined }); // '{}' — lost key
Fine for quick tests on controlled plain data; not a general equal.
UI guidance
| Situation | Prefer |
|---|---|
| Event handler identity | stable useCallback / ref |
| Props of memoized child | shallow; keep props flat |
| Normalized store entities | compare by id + version |
| Huge nested trees | structural sharing (immutable libs) so === works |
// good: parent keeps same item refs when data unchanged
// bad: items={data.map(...)} every render with new objects → shallow always fails
Interview answer (out loud)
“=== is reference equality. Shallow equal compares one level of keys with Object.is on values — what React.memo uses. Deep equal walks nested structure, costs more, and needs cycle handling. I avoid JSON.stringify for equality and design state so reference stability makes shallow checks useful.”
Further reading
Related
Related guides
- AbortControllerCancel fetch and other async work with AbortController and AbortSignal — timeouts, race conditions, and cleanup when components unmount.
- Array find, some, every, includesShort-circuiting array predicates: find, findIndex, some, every, and includes — when to use each and common interview traps.
- Array map, filter, reducemap, filter, and reduce as the core transform toolkit — immutability, chaining costs, reduce patterns, and when a plain loop is clearer.
- Arrow Functions Deep DiveLexical this, no arguments object, no construct, concise bodies — when arrows help and when methods and generators need classic functions.
- Async Iteration and for await...ofAsync iterables, for await...of, and streaming data — how async iterators differ from Promise.all and when to use each.