You've probably seen code like this in React:
<ChildComponent config={{ theme: 'dark' }} />
And wondered why ChildComponent re-renders on every parent render, even when wrapped in React.memo.
Or you've used useEffect and seen it run repeatedly:
useEffect(() => {
fetchData();
}, [config]); // 'config' is recreated every render!
The culprit? Referential equality — JavaScript's way of comparing objects and arrays by reference, not by value.
Understanding this is critical for writing performant React applications.
The Golden Rule
In JavaScript, objects and arrays are compared by reference (memory address), not by value. Two objects with identical contents are NOT equal if they're different instances. This means {} !== {} and [] !== []. React uses referential equality to determine when to re-render components, so creating new objects/arrays on every render can cause unnecessary re-renders.
In simpler terms: JavaScript doesn't care if two objects look the same — it only cares if they're the SAME object in memory.
Let's understand why this matters and how memoization solves it.
Part 1: Referential Equality Explained
Primitives vs Objects
Primitives (numbers, strings, booleans, null, undefined) are compared by value:
const a = 5;
const b = 5;
console.log(a === b); // true
const str1 = 'hello';
const str2 = 'hello';
console.log(str1 === str2); // true
Objects (including arrays, functions) are compared by reference:
const obj1 = { name: 'Alice' };
const obj2 = { name: 'Alice' };
console.log(obj1 === obj2); // false (different references!)
const arr1 = [1, 2, 3];
const arr2 = [1, 2, 3];
console.log(arr1 === arr2); // false
Why? obj1 and obj2 are stored at different memory addresses, even though they have identical contents.
What About Same Reference?
const obj1 = { name: 'Alice' };
const obj2 = obj1; // Same reference
console.log(obj1 === obj2); // true (same memory address)
obj2.name = 'Bob';
console.log(obj1.name); // "Bob" (mutation affects both!)
Key Point: When you assign an object to another variable, you're copying the reference, not the object itself. (See the "Pass by Value" article for more on this!)
Part 2: How This Affects React
React uses referential equality to determine if props have changed:
Example: React.memo Doesn't Work
function Child({ config }) {
console.log('Child rendered');
return <div>{config.theme}</div>;
}
const MemoizedChild = React.memo(Child);
function Parent() {
const [count, setCount] = useState(0);
const config = { theme: 'dark' }; // New object every render!
return (
<div>
<button onClick={() => setCount(count + 1)}>Count: {count}</button>
<MemoizedChild config={config} />
</div>
);
}
Result: Every time you click the button, MemoizedChild re-renders, even though React.memo is supposed to prevent that.
Why?
- Clicking the button triggers
Parentto re-render -
const config = { theme: 'dark' }creates a new object - React compares the old and new
configprops:oldConfig === newConfig→ false - React thinks the props changed, so it re-renders
MemoizedChild
The Same Problem with Arrays
function Parent() {
const [count, setCount] = useState(0);
const items = [1, 2, 3]; // New array every render!
return (
<div>
<button onClick={() => setCount(count + 1)}>Count: {count}</button>
<MemoizedList items={items} />
</div>
);
}
Same issue: items is a new array on every render, so MemoizedList always re-renders.
And Functions Too!
function Parent() {
const [count, setCount] = useState(0);
const handleClick = () => { // New function every render!
console.log('Clicked');
};
return (
<div>
<button onClick={() => setCount(count + 1)}>Count: {count}</button>
<MemoizedButton onClick={handleClick} />
</div>
);
}
Same issue: handleClick is a new function on every render.
Part 3: Memoization to the Rescue
Memoization is the technique of caching values/computations so they're only recalculated when dependencies change.
React provides three hooks for memoization:
-
useMemo— Memoize values (objects, arrays, expensive computations) -
useCallback— Memoize functions (special case ofuseMemo) -
React.memo— Memoize entire components
1. useMemo for Objects and Arrays
function Parent() {
const [count, setCount] = useState(0);
const config = useMemo(() => ({ theme: 'dark' }), []); // Same object every render!
return (
<div>
<button onClick={() => setCount(count + 1)}>Count: {count}</button>
<MemoizedChild config={config} />
</div>
);
}
How it works:
-
useMemoreturns the same object ({ theme: 'dark' }) on every render - The object is only recreated if dependencies change (empty array
[]= never) - Now
oldConfig === newConfig→ true, soMemoizedChilddoesn't re-render
With Dependencies
function Parent({ theme }) {
const [count, setCount] = useState(0);
const config = useMemo(() => ({ theme }), [theme]); // Recreate only when 'theme' changes
return (
<div>
<button onClick={() => setCount(count + 1)}>Count: {count}</button>
<MemoizedChild config={config} />
</div>
);
}
Now:
- When
countchanges,configstays the same (no re-render) - When
themechanges,configis recreated (re-render happens, as expected)
2. useCallback for Functions
useCallback is shorthand for memoizing functions:
function Parent() {
const [count, setCount] = useState(0);
const handleClick = useCallback(() => { // Same function every render!
console.log('Clicked');
}, []); // No dependencies = function never changes
return (
<div>
<button onClick={() => setCount(count + 1)}>Count: {count}</button>
<MemoizedButton onClick={handleClick} />
</div>
);
}
Equivalent useMemo version:
const handleClick = useMemo(() => {
return () => console.log('Clicked');
}, []);
But useCallback is cleaner for functions.
With Dependencies
function Parent({ userId }) {
const [count, setCount] = useState(0);
const handleClick = useCallback(() => {
console.log(`Clicked for user ${userId}`);
}, [userId]); // Recreate when 'userId' changes
return (
<div>
<button onClick={() => setCount(count + 1)}>Count: {count}</button>
<MemoizedButton onClick={handleClick} />
</div>
);
}
3. React.memo for Components
React.memo is a Higher-Order Component that memoizes the entire component:
const Child = React.memo(function Child({ config }) {
console.log('Child rendered');
return <div>{config.theme}</div>;
});
How it works:
- React shallowly compares old and new props
- If all props are referentially equal, skip re-render
- If any prop changes (by reference), re-render
Important: React.memo uses shallow comparison:
// Shallow comparison (default)
oldProps.config === newProps.config // Checks reference only
// Deep comparison (custom)
React.memo(Child, (prevProps, nextProps) => {
return prevProps.config.theme === nextProps.config.theme; // Custom logic
});
Part 4: When to Use Memoization
Use Memoization When:
- Passing objects/arrays to memoized child components
const config = useMemo(() => ({ theme: 'dark' }), []);
<MemoizedChild config={config} />
- Passing functions to memoized child components
const handleClick = useCallback(() => {}, []);
<MemoizedButton onClick={handleClick} />
- Expensive computations
const sortedData = useMemo(() => {
return data.sort((a, b) => a.value - b.value); // Expensive!
}, [data]);
- Dependencies in
useEffector other hooks
const config = useMemo(() => ({ theme: 'dark' }), []);
useEffect(() => {
fetchData(config); // Won't re-run unnecessarily
}, [config]);
Don't Use Memoization When:
- Primitives (already compared by value)
// Unnecessary
const count = useMemo(() => 5, []);
// Just use the value
const count = 5;
- Child doesn't re-render expensively
// Overkill if rendering <div>{text}</div> is cheap
const text = useMemo(() => someString, []);
- Component always re-renders anyway
// No benefit if Child isn't memoized
const config = useMemo(() => ({ theme: 'dark' }), []);
<Child config={config} /> // Not wrapped in React.memo
- Premature optimization
Don't memoize everything! Only optimize when you have a performance problem.
Part 5: Common Memoization Pitfalls
Pitfall 1: Missing Dependencies
function Component({ userId }) {
const handleClick = useCallback(() => {
console.log(`User: ${userId}`); // Uses 'userId'
}, []); // Missing dependency!
return <button onClick={handleClick}>Click</button>;
}
Problem: handleClick closes over the initial userId and never updates.
Fix:
const handleClick = useCallback(() => {
console.log(`User: ${userId}`);
}, [userId]); // Include 'userId'
Pitfall 2: Memoizing Without Memoized Child
function Parent() {
const config = useMemo(() => ({ theme: 'dark' }), []); // Useless!
return <Child config={config} />; // Child isn't memoized
}
function Child({ config }) {
return <div>{config.theme}</div>;
}
Problem: Child re-renders on every Parent render anyway (not wrapped in React.memo).
Fix:
const MemoizedChild = React.memo(Child); // Now useMemo helps
function Parent() {
const config = useMemo(() => ({ theme: 'dark' }), []);
return <MemoizedChild config={config} />;
}
Pitfall 3: Creating Objects Inside JSX
function Parent() {
return <Child config={{ theme: 'dark' }} />; // New object every render!
}
Fix:
function Parent() {
const config = useMemo(() => ({ theme: 'dark' }), []);
return <Child config={config} />; // Stable reference
}
Pitfall 4: Memoizing Everything (Over-Optimization)
// Too much memoization
function Component() {
const a = useMemo(() => 1, []);
const b = useMemo(() => 2, []);
const sum = useMemo(() => a + b, [a, b]);
return <div>{sum}</div>;
}
Problem: useMemo has overhead. For simple computations, it's slower than just recalculating.
Better:
function Component() {
const a = 1;
const b = 2;
const sum = a + b; // Fast enough without memoization
return <div>{sum}</div>;
}
Part 6: Memoization Patterns in React
Pattern 1: Memoizing Context Values
function ThemeProvider({ children }) {
const [theme, setTheme] = useState('light');
// New object every render → all consumers re-render!
// const value = { theme, setTheme };
// Stable reference
const value = useMemo(() => ({ theme, setTheme }), [theme]);
return (
<ThemeContext.Provider value={value}>
{children}
</ThemeContext.Provider>
);
}
Pattern 2: Memoizing Expensive Filters
function UserList({ users, filter }) {
const filteredUsers = useMemo(() => {
return users.filter(user => {
// Expensive filtering logic
return user.name.toLowerCase().includes(filter.toLowerCase());
});
}, [users, filter]); // Only recompute when 'users' or 'filter' changes
return (
<ul>
{filteredUsers.map(user => (
<li key={user.id}>{user.name}</li>
))}
</ul>
);
}
Pattern 3: Memoizing Event Handlers with Parameters
function ItemList({ items, onDelete }) {
return (
<ul>
{items.map(item => (
<Item key={item.id} item={item} onDelete={onDelete} />
))}
</ul>
);
}
const Item = React.memo(function Item({ item, onDelete }) {
// New function every render
// const handleDelete = () => onDelete(item.id);
// Memoized
const handleDelete = useCallback(() => {
onDelete(item.id);
}, [item.id, onDelete]);
return (
<li>
{item.name}
<button onClick={handleDelete}>Delete</button>
</li>
);
});
Pattern 4: Using Refs for Stable References
Sometimes you need a value that doesn't trigger re-renders:
function Component() {
const configRef = useRef({ theme: 'dark' });
useEffect(() => {
// Always uses the same object reference
fetchData(configRef.current);
}, []); // No dependencies needed
return <div>Content</div>;
}
Use case: When you need a stable reference but don't want to trigger re-renders when it changes.
Part 7: Debugging Referential Equality
Tool 1: Object.is() (React's Comparison)
React uses Object.is() for comparison (similar to === but handles NaN correctly):
const obj1 = { theme: 'dark' };
const obj2 = { theme: 'dark' };
console.log(Object.is(obj1, obj2)); // false (different references)
const obj3 = obj1;
console.log(Object.is(obj1, obj3)); // true (same reference)
Tool 2: React DevTools Profiler
Use the React DevTools Profiler to see why components re-render:
- Open React DevTools
- Go to Profiler tab
- Start recording
- Interact with your app
- Look at "Why did this render?" section
Tool 3: useWhyDidYouUpdate Hook (Custom)
function useWhyDidYouUpdate(name, props) {
const previousProps = useRef();
useEffect(() => {
if (previousProps.current) {
const allKeys = Object.keys({ ...previousProps.current, ...props });
const changedProps = {};
allKeys.forEach(key => {
if (previousProps.current[key] !== props[key]) {
changedProps[key] = {
from: previousProps.current[key],
to: props[key]
};
}
});
if (Object.keys(changedProps).length) {
console.log('[why-did-you-update]', name, changedProps);
}
}
previousProps.current = props;
});
}
function MyComponent(props) {
useWhyDidYouUpdate('MyComponent', props);
return <div>Content</div>;
}
Quick Reference Cheat Sheet
| Hook | Purpose | Example |
|---|---|---|
useMemo |
Memoize values (objects, arrays, computations) | useMemo(() => ({ theme }), [theme]) |
useCallback |
Memoize functions | useCallback(() => {}, [deps]) |
React.memo |
Memoize components | React.memo(Component) |
useRef |
Stable mutable reference (doesn't cause re-renders) | useRef({ theme: 'dark' }) |
Key Takeaways
Objects and arrays are compared by reference, not by value: {} !== {}
Creating objects/arrays/functions on every render causes unnecessary re-renders in memoized components
useMemo stabilizes object/array references across renders
useCallback stabilizes function references (shorthand for useMemo with functions)
React.memo prevents component re-renders when props don't change (by reference)
Always include dependencies in useMemo and useCallback
Don't memoize primitives — they're already compared by value
Memoize context values to prevent unnecessary consumer re-renders
Don't over-optimize — memoization has overhead, only use it when needed
Interview Tip
When asked about referential equality and memoization:
- "In JavaScript, objects are compared by reference, not value, so
{} !== {}even though they look identical" - React's impact: "React uses referential equality to check if props changed. If you create a new object on every render, React thinks the props changed, causing re-renders"
- Solution: "
useMemoanduseCallbackstabilize references across renders, preventing unnecessary re-renders in memoized components" - Example: "If you pass
config={{ theme: 'dark' }}to a memoized child, it re-renders every time. UsinguseMemo(() => ({ theme: 'dark' }), [])fixes it" - Best practice: "Only memoize when you have a performance problem — measure first, optimize second"
Now go forth and never wonder why your memoized component keeps re-rendering again!h
Top comments (0)