Back to React
React
medium
mid

What is the difference between shallow and deep comparison in React's shouldComponentUpdate?

Shallow comparison checks reference equality for objects/arrays and value equality for primitives — what React.memo and PureComponent do by default. Deep comparison walks every nested key recursively. Shallow is cheap (O(top-level props)) but misses changes inside nested objects; deep catches everything but is O(size of tree) and can be slower than re-rendering. Use shallow + immutability discipline; reach for deep only with cause.

6 min read·~5 min to think through

Both compare prev vs next props/state. The difference is depth.

Shallow

For each prop:

  • If primitive (number, string, bool): compare values.
  • If object/array: compare references.
js
shallowEqual({ a: 1, b: { x: 2 } }, { a: 1, b: { x: 2 } })
// false — b is a different reference

This is what React.memo's default compare does, and what PureComponent did.

Deep

Recursively compare all keys.

js
deepEqual({ a: 1, b: { x: 2 } }, { a: 1, b: { x: 2 } })
// true

Implementations: fast-deep-equal, lodash.isEqual. Both walk every nested key.

Tradeoffs

ShallowDeep
CostO(number of props) — microsecondsO(size of nested tree) — milliseconds for big objects
False negativesReports 'changed' when only reference differsRare
False positivesIf you mutate nested objects in placeIf two references to the same object are passed
Common useReact.memo, PureComponentCustom memo with deep comparator

Why shallow is the default

React optimizes for the case where:

  • Components produce new objects on update (immutability).
  • Unchanged data keeps its reference.
  • New data gets a new reference.

This makes shallow checks fast AND correct, as long as you don't mutate.

Mutation breaks shallow

tsx
const [users, setUsers] = useState<User[]>([...]);

function addUser(u: User) {
  users.push(u);          // BAD — same reference
  setUsers(users);        // React: no change, won't re-render
}

function addUserCorrect(u: User) {
  setUsers([...users, u]); // GOOD — new reference
}

When deep comparison helps

  • You receive props from a source you don't control (legacy code, JSON parses).
  • A complex object is reconstructed identically each time but is referentially new.
  • You absolutely cannot achieve referential stability in the source.
tsx
const Comp = memo(MyComp, (prev, next) => isEqual(prev, next));

When deep is worse

  • The object is large — comparison costs more than re-rendering.
  • The component is cheap — bookkeeping outweighs savings.
  • You're using deep to paper over a mutation bug — fix the bug instead.

Senior framing

Shallow comparison + immutability is the React idiom. Deep comparison is a workaround when immutability isn't enforced. Both have their place but defaulting to shallow with disciplined updates is what scales — deep comparators on hot components become the bottleneck.

Historical note

In class components, shouldComponentUpdate(nextProps, nextState) could implement either. PureComponent did shallow automatically. The hooks era replaced this with React.memo (function components) using the same shallow default.

Follow-up questions

  • Why does mutation break React.memo?
  • When would you write a custom comparator function for memo?
  • How does Immer help with shallow-compatible immutable updates?

Common mistakes

  • Mutating arrays/objects in state — shallow checks miss the change.
  • Using deep comparison everywhere — gives up the perf benefit.
  • Trying to write deep equal by hand — there are subtle bugs (Date, RegExp, Map).

Performance considerations

  • Shallow: ~10-100 ns per prop. Deep: scales with tree size — can be milliseconds for big trees. The break-even is when the saved render is more expensive than the deep walk. Profile.

Edge cases

  • NaN !== NaN in === but should be equal — use Object.is.
  • Functions can't be deep-equaled meaningfully.
  • Cycles in objects break naive deep equal.

Real-world examples

  • Redux uses === (reference) comparison for its connect HOC by default. React.memo is shallow. Immer/Redux Toolkit make immutability ergonomic to preserve shallow correctness.

Senior engineer discussion

Senior framing: shallow vs deep is downstream of mutability. The right way to think: pick immutability as a discipline (or use Immer to fake it), then shallow comparison just works. Deep comparators are an escape hatch, not a default.

Related questions