TL;DR / JavaScript's
===compares object identity (memory address), not contents — and React uses this comparison to decide whether to re-render.
How It Works
React re-render decision (React.memo)
┌───────────┐ ┌───────┐ ┌───────────┐
│ prevProps │───────→│ === ? │←───────│ nextProps │
└───────────┘ └───────┘ └───────────┘
│
same ref │ new ref
┌───────────────└────────────────┐
│ │
↓ ↓
┌─────────────┐ ┌───────────┐
│ Skip render │ │ Re-render │
└─────────────┘ └───────────┘
Referential equality means two values are the same reference — the same slot in memory. In JavaScript, === for primitives (strings, numbers, booleans) compares values directly: "hello" === "hello" is true. But for objects, arrays, and functions, === compares identity: two objects with identical contents are not equal unless they are literally the same object.
const a = { x: 1 };
const b = { x: 1 };
a === b; // false — different objects
const c = a;
a === c; // true — same reference
This distinction is the foundation of React's performance model.
React's Reliance on Reference Checks
React uses referential equality everywhere. When a parent component re-renders and passes props to a child wrapped in React.memo, React compares each prop from the previous render to the current render using ===. If all props are referentially equal, the child skips rendering entirely.
The same mechanism drives useEffect — React compares each dependency with === to decide whether the effect should re-run. It drives useMemo and useCallback — deps are compared by reference. And it drives useSelector in Redux — the selector's return value is compared by reference to decide whether the subscribing component re-renders.
The Inline Object Problem
The most common referential equality trap is creating new objects or arrays inline during render:
// New object every render — memo is useless
<Child style={{ color: "red" }} />
// New array every render
<List items={data.filter(d => d.active)} />
// New function every render
<Button onClick={() => handleClick(id)} />
Each render creates a fresh reference, so === always returns false, and memoization never kicks in. The child re-renders every time the parent does, regardless of whether the actual data changed.
Stabilizing References
To preserve referential equality across renders, you hoist values out of the render path or memoize them:
// Stable object — defined outside component or in useMemo
const style = useMemo(() => ({ color: "red" }), []);
// Stable function — useCallback preserves reference
const handleClick = useCallback(() => doThing(id), [id]);
// Stable derived data
const activeItems = useMemo(() => data.filter(d => d.active), [data]);
The key insight is that memoization is about identity, not computation. Even if filtering an array is cheap to compute, creating a new array reference on every render defeats downstream memoization.
Selectors and Derived State
In state management, selectors that return new objects are a major source of unnecessary re-renders. Consider a Redux selector:
const selectUser = state => ({
name: state.user.name,
role: state.user.role
});
This creates a new object every time the store updates — even if name and role haven't changed. Libraries like Reselect solve this with memoized selectors that return the previous result if the inputs haven't changed, preserving referential equality.
Structural Equality vs. Referential Equality
Sometimes you genuinely need to compare by value rather than by reference. Libraries like Lodash provide _.isEqual for deep comparison, and some React utilities accept custom comparators. React.memo takes an optional second argument:
React.memo(Component, (prev, next) => {
return prev.data.id === next.data.id;
});
But deep equality checks are O(n) in the size of the data, while reference checks are O(1). Immutable data patterns and structural sharing exist precisely to make reference checks reliable — if the reference is the same, the data is guaranteed unchanged.
The Compiler Approach
React Compiler (formerly React Forget) automates reference stabilization. It analyzes your component code at build time and inserts memoization automatically, ensuring that objects, arrays, and functions created during render maintain stable references when their inputs haven't changed. This eliminates the need for manual useMemo and useCallback in many cases.
Gotchas
{} !== {}— every object literal creates a new reference. Passing object literals as props to memoized components defeats memoization entirely.useEffectwith object deps causes infinite loops if the object is recreated each render. Either memoize the object or destructure to primitive deps.NaN !== NaNin JavaScript, but React's dependency comparison usesObject.is, whereObject.is(NaN, NaN)returnstrue. This meansNaNin deps will not trigger re-runs.- String and number props are safe — primitives are compared by value, so
<Child count={5} />will correctly skip re-render when count stays5. - Context value objects must be memoized.
<Ctx.Provider value={{ user }}>creates a new object each render, causing all consumers to re-render even ifuserhasn't changed.