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.
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.
shallowEqual({ a: 1, b: { x: 2 } }, { a: 1, b: { x: 2 } })
// false — b is a different referenceThis is what React.memo's default compare does, and what PureComponent did.
Deep
Recursively compare all keys.
deepEqual({ a: 1, b: { x: 2 } }, { a: 1, b: { x: 2 } })
// trueImplementations: fast-deep-equal, lodash.isEqual. Both walk every nested key.
Tradeoffs
| Shallow | Deep | |
|---|---|---|
| Cost | O(number of props) — microseconds | O(size of nested tree) — milliseconds for big objects |
| False negatives | Reports 'changed' when only reference differs | Rare |
| False positives | If you mutate nested objects in place | If two references to the same object are passed |
| Common use | React.memo, PureComponent | Custom 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
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.
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.