ESC

Type to search the knowledge base.

Shallow vs Deep Equality

Compare references vs structure — React prop checks, writing shallowEqual, deep equal costs, and JSON.stringify traps.

intermediate3 min read
  • 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 guides