`useMemo` for Memoizing Values (Part 2): Referential Equality and Hooks #125
📖 Introduction
In Part 1 of our useMemo
exploration, we focused on how useMemo
can optimize React components by caching the results of expensive calculations. This prevents redundant computations when a component re-renders for reasons unrelated to those calculations.
However, useMemo
has another equally crucial role: preserving referential equality for objects and arrays. This is vital for effectively using React.memo
to optimize child components and for ensuring stable dependencies in other hooks like useEffect
and useCallback
. In this article, we'll dive deep into this aspect of useMemo
, illustrating with practical examples how it helps prevent unnecessary re-renders and hook executions.
📚 Prerequisites
Before proceeding, ensure you are comfortable with:
- All concepts from
useMemo
for Memoizing Expensive Calculations (Part 1). React.memo
for memoizing components.- JavaScript's distinction between primitive types (strings, numbers, booleans) and reference types (objects, arrays, functions).
- Basic understanding of
useEffect
anduseCallback
hooks (though we'll touch upon their interaction withuseMemo
).
🎯 Article Outline: What You'll Master
In this article, you will learn:
- ✅ Referential Equality Deep Dive: Understanding why
{} !== {}
and how this affects React. - ✅
useMemo
withReact.memo
: HowuseMemo
enablesReact.memo
to work effectively when passing object or array props. - ✅ Practical Application: Building components where
useMemo
prevents unnecessary re-renders of memoized children. - ✅
useMemo
for Hook Dependencies: Stabilizing object/array dependencies foruseEffect
anduseCallback
. - ✅ Best Practices & Anti-Patterns: Specifically for referential equality scenarios.
🧠 Section 1: Referential Equality Explained – The "Why"
In JavaScript, variables can hold either primitive values or references to objects/arrays/functions.
-
Primitive Types (string, number, boolean, null, undefined, symbol, bigint): When you compare primitive types, JavaScript compares their actual values.
let a = 5;
let b = 5;
console.log(a === b); // true
let str1 = "hello";
let str2 = "hello";
console.log(str1 === str2); // true -
Reference Types (object, array, function): When you work with these types, the variable stores a reference (like an address in memory) to where the actual object/array/function is stored. Comparing reference types with
===
checks if they point to the exact same location in memory, not just if they have the same contents.let obj1 = { name: "Alice" };
let obj2 = { name: "Alice" };
console.log(obj1 === obj2); // false - they look the same, but are two separate objects in memory
let arr1 = [1, 2, 3];
let arr2 = [1, 2, 3];
console.log(arr1 === arr2); // false - two separate arrays
let sameObj = obj1;
console.log(obj1 === sameObj); // true - sameObj points to the exact same object as obj1
How This Impacts React Props and Re-renders:
When a parent component re-renders, its function body re-executes. If you define an object or array literal directly within the render function, a new object or array is created on every single render, even if the data it contains is identical to the previous render.
function ParentComponent() {
const [counter, setCounter] = useState(0);
// On every render of ParentComponent, a NEW styleObject is created
const styleObject = { color: 'blue', fontSize: '16px' };
// On every render, a NEW userDetails object is created
const userDetails = { id: 1, name: "Admin" };
return (
<div>
<button onClick={() => setCounter(c => c + 1)}>Re-render Parent: {counter}</button>
{/* ChildComponent will receive a NEW styleObject reference on every ParentComponent render */}
<MemoizedChildComponent styleProp={styleObject} user={userDetails} />
</div>
);
}
const MemoizedChildComponent = React.memo(function ChildComponent({ styleProp, user }) {
console.log("MemoizedChildComponent rendered");
return <div style={styleProp}>User: {user.name}</div>;
});
In the ParentComponent
above, even if MemoizedChildComponent
is wrapped with React.memo
, it will still re-render every time ParentComponent
re-renders. Why? Because React.memo
does a shallow comparison of props. Since styleObject
and userDetails
are new objects (new references) on each render of ParentComponent
, React.memo
sees them as "changed" props, thus triggering a re-render of MemoizedChildComponent
.
This is where useMemo
becomes essential for objects and arrays passed as props.
💻 Section 2: useMemo
to Preserve Referential Equality for Props
useMemo
can be used to memoize an object or array, ensuring that the same reference is returned on subsequent renders if the dependencies used to create that object/array haven't changed.
2.1 - The Problem: React.memo
Child Re-rendering Unnecessarily
Let's refine the example above to clearly show the problem.
// code-block-1.jsx
// Demonstrating a React.memo child re-rendering due to unstable object props.
import React, { useState, memo } from 'react';
const UserDisplay = memo(function UserDisplay({ userConfig }) {
// This log will show us when UserDisplay re-renders
console.log(`UserDisplay rendered with config for: ${userConfig.name}`);
return (
<div style={userConfig.style}>
<p>Name: {userConfig.name}</p>
<p>Role: {userConfig.role}</p>
</div>
);
});
function App() {
const [theme, setTheme] = useState('light'); // Unrelated state to trigger App re-renders
// This userConfig object is RECREATED on EVERY App render
const adminUserConfig = {
name: 'Admin User',
role: 'Administrator',
style: {
padding: '10px',
border: '1px solid blue',
backgroundColor: theme === 'light' ? '#e0e0ff' : '#303050', // Style depends on theme
}
};
return (
<div>
<h2>User Profile</h2>
<button onClick={() => setTheme(t => t === 'light' ? 'dark' : 'light')}>
Toggle Theme (Re-renders App)
</button>
<p>Current theme: {theme}</p>
<UserDisplay userConfig={adminUserConfig} />
<p>
<em>
Open your console. Notice "UserDisplay rendered..." logs every time you toggle the theme,
even though the user's name and role haven't changed. This is because
`adminUserConfig` is a new object reference each time.
</em>
</p>
</div>
);
}
export default App;
Code Breakdown & Problem Explanation:
UserDisplay
: A component wrapped withReact.memo
. It expects to only re-render if itsuserConfig
prop changes.App
Component:- Has an unrelated
theme
state. Toggling the theme re-rendersApp
. adminUserConfig
: This object is defined directly insideApp
's render function. Every timeApp
re-renders (e.g., due totheme
change), a brand newadminUserConfig
object is created in memory.
- Has an unrelated
- The Issue: Even though the content of
adminUserConfig
(likename
androle
) might be the same across renders (unlesstheme
changes the style), theUserDisplay
component receives a new object reference foruserConfig
each timeApp
re-renders.React.memo
performs a shallow comparison:prevProps.userConfig === nextProps.userConfig
will befalse
because they are different objects in memory. Thus,UserDisplay
re-renders.
2.2 - The Solution: Stabilizing Prop References with useMemo
Now, let's apply useMemo
to stabilize the adminUserConfig
object.
// code-block-2.jsx
// Using useMemo to stabilize object props for a React.memo child.
import React, { useState, memo, useMemo } from 'react';
const UserDisplay = memo(function UserDisplay({ userConfig }) {
console.log(`UserDisplay rendered with config for: ${userConfig.name}`);
return (
<div style={userConfig.style}>
<p>Name: {userConfig.name}</p>
<p>Role: {userConfig.role}</p>
</div>
);
});
function App() {
const [theme, setTheme] = useState('light');
// Memoize the adminUserConfig object
const adminUserConfig = useMemo(() => {
// This function now only re-runs if 'theme' changes
console.log("Recalculating adminUserConfig object");
return {
name: 'Admin User',
role: 'Administrator',
style: {
padding: '10px',
border: '1px solid blue',
backgroundColor: theme === 'light' ? '#e0e0ff' : '#303050',
}
};
}, [theme]); // Dependency: only recalculate if 'theme' changes
return (
<div>
<h2>User Profile</h2>
<button onClick={() => setTheme(t => t === 'light' ? 'dark' : 'light')}>
Toggle Theme (Re-renders App)
</button>
<p>Current theme: {theme}</p>
<UserDisplay userConfig={adminUserConfig} />
<p>
<em>
Open your console. "Recalculating adminUserConfig object" and "UserDisplay rendered..."
now ONLY log when the theme is actually toggled (because the style sub-object changes).
If theme didn't affect the style, UserDisplay wouldn't re-render at all from theme toggles.
</em>
</p>
</div>
);
}
export default App;
Code Breakdown & Solution Explanation:
adminUserConfig = useMemo(() => { ... }, [theme]);
:- The creation of the
adminUserConfig
object is now wrapped inuseMemo
. - The function provided to
useMemo
returns the configuration object. - The dependency array is
[theme]
. This means the function (and thus the object creation) will only re-run if thetheme
state changes.
- The creation of the
- The Result:
- When
App
re-renders buttheme
has not changed,useMemo
returns the exact sameadminUserConfig
object reference as the previous render. - Now, when
React.memo
inUserDisplay
comparesprevProps.userConfig === nextProps.userConfig
, it will betrue
(iftheme
didn't change). - Consequently,
UserDisplay
will correctly skip re-rendering if its underlying data (driven bytheme
in this case) hasn't changed, even ifApp
re-renders for other reasons (if we had other unrelated state inApp
). - In this specific example, changing the theme does change the
style
property withinadminUserConfig
, soadminUserConfig
is a new object, andUserDisplay
re-renders. This is correct. Iftheme
was not a dependency for thestyle
(e.g., ifbackgroundColor
was fixed), then toggling theme would not causeUserDisplay
to re-render.
- When
useMemo
here ensures that UserDisplay
only re-renders when the data it truly depends on (via adminUserConfig
) actually changes, not just because its parent re-rendered and created a new object reference.
🛠️ Section 3: Project-Based Example: Optimizing a List of Memoized Components
Let's build a ColorSwatchList
where each ColorSwatch
is a React.memo
-wrapped component. Each ColorSwatch
will receive a config
object prop defining its color and label. We'll demonstrate how useMemo
is vital in the parent ColorSwatchList
to prevent all swatches from re-rendering when the list itself doesn't change but the parent re-renders.
The Goal:
Create a list of color swatches. Each swatch component is memoized. Ensure that if the parent component re-renders for an unrelated reason, the individual swatch components (whose props haven't actually changed in value) do not re-render, by stabilizing their config
prop references using useMemo
.
The Plan:
- Create a
ColorSwatch
component, wrapped inReact.memo
. It takes aconfig
object prop. - Create a
ColorSwatchList
component that maps over an array of color data to renderColorSwatch
components. - Initially, the
config
objects will be created directly in themap
function (unstable references). - Add an unrelated state to
ColorSwatchList
's parent to trigger re-renders. - Observe that all
ColorSwatch
components re-render. - Refactor
ColorSwatchList
to useuseMemo
(potentially an array of memoized config objects, or memoizing the map operation if the source data is stable).
// project-example.jsx
// Optimizing a list of memoized children with useMemo for their props.
import React, { useState, memo, useMemo } from 'react';
const ColorSwatch = memo(function ColorSwatch({ swatchConfig }) {
console.log(`ColorSwatch rendered: ${swatchConfig.label}`);
return (
<div style={{
width: '100px',
height: '100px',
backgroundColor: swatchConfig.color,
margin: '5px',
display: 'flex',
alignItems: 'center',
justifyContent: 'center',
border: '1px solid #ccc'
}}>
{swatchConfig.label}
</div>
);
});
const initialColorData = [
{ id: 'c1', color: 'red', label: 'Red' },
{ id: 'c2', color: 'green', label: 'Green' },
{ id: 'c3', color: 'blue', label: 'Blue' },
];
function ColorSwatchList({ colorData }) {
// If we create config objects directly in map, they are new on each render:
// const swatches = colorData.map(data => <ColorSwatch key={data.id} swatchConfig={{ color: data.color, label: data.label }} />);
// With useMemo, we create an array of STABLE config objects,
// but only if colorData itself changes.
const memoizedSwatches = useMemo(() => {
console.log("Recalculating memoizedSwatches for ColorSwatchList");
return colorData.map(data => (
<ColorSwatch key={data.id} swatchConfig={{ color: data.color, label: data.label }} />
));
// A more robust approach if swatchConfig itself needs to be stable for each item:
// return colorData.map(data => {
// const stableSwatchConfig = // another useMemo for individual item if data.color/label could be complex objects
// { color: data.color, label: data.label };
// return <ColorSwatch key={data.id} swatchConfig={stableSwatchConfig} />;
// });
// For this example, if colorData reference is stable, and its contents are primitive,
// memoizing the map output is often sufficient.
// However, the most common pattern is to memoize the prop object itself.
// Let's refine to memoize each config object if `colorData` items are stable.
}, [colorData]);
// Refined: Better approach is to pass stable swatchConfig to each ColorSwatch
// This requires colorData itself to be stable or for each item to be memoized.
// For simplicity of this example, if 'colorData' prop is stable, this is okay.
// A more complex scenario would involve memoizing individual config objects if they were derived.
// Let's make an array of memoized config objects
const swatchConfigs = useMemo(() => {
console.log("Recalculating swatchConfigs array");
return colorData.map(data => ({
// Assuming data.color and data.label are primitive and stable within data object
color: data.color,
label: data.label
}));
}, [colorData]);
return (
<div style={{ display: 'flex', flexWrap: 'wrap' }}>
{/* {swatches} Using the unoptimized version would cause re-renders */}
{/* {memoizedSwatches} Using the version that memoizes the whole map output */}
{swatchConfigs.map((config, index) => (
<ColorSwatch key={initialColorData[index].id} swatchConfig={config} />
))}
</div>
);
}
function App() {
const [appRefreshCounter, setAppRefreshCounter] = useState(0);
// To ensure 'initialColorData' has a stable reference if it were from state/props
const stableColorData = useMemo(() => initialColorData, []);
return (
<div>
<h2>Color Swatch Demo</h2>
<button onClick={() => setAppRefreshCounter(c => c + 1)}>
Force App Re-render (Counter: {appRefreshCounter})
</button>
<ColorSwatchList colorData={stableColorData} />
<p>
<em>
Open console. When "Force App Re-render" is clicked:
- "Recalculating swatchConfigs array" should NOT log (because stableColorData doesn't change).
- "ColorSwatch rendered..." for each swatch should NOT log,
proving `React.memo` and stable props from `useMemo` are working.
</em>
</p>
</div>
);
}
export default App;
Walkthrough & Explanation:
ColorSwatch
: AReact.memo
-wrapped component. It re-renders ifswatchConfig
changes reference.initialColorData
: Our source data. InApp
, we wrap it withuseMemo(() => initialColorData, [])
to ensurestableColorData
has a persistent reference acrossApp
re-renders. This simulates receiving stable prop data.ColorSwatchList
:- Receives
colorData
. swatchConfigs = useMemo(...)
: This is key. We map overcolorData
to create an array ofswatchConfig
objects. This entire array of config objects is memoized. It will only be recalculated if thecolorData
prop itself changes reference.- Inside the render, we map over
swatchConfigs
. EachColorSwatch
receives aswatchConfig
object from this memoized array.
- Receives
App
Component:appRefreshCounter
state is used to forceApp
to re-render without changingstableColorData
.
- Optimization in Action:
- When you click "Force App Re-render":
App
re-renders.ColorSwatchList
receives the samestableColorData
reference.- Because
colorData
prop inColorSwatchList
hasn't changed, theuseMemo
forswatchConfigs
returns the cached array of config objects. - Each
ColorSwatch
component receives the sameswatchConfig
object reference as in the previous render. - Thanks to
React.memo
and the stable prop references, theColorSwatch
components correctly skip re-rendering. The console logs for individual swatches will not appear. - The "Recalculating swatchConfigs array" log will also not appear after the initial render, because
stableColorData
's reference is stable.
- When you click "Force App Re-render":
This project highlights how useMemo
is critical for optimizing lists of memoized components by ensuring the props passed to them (especially objects or arrays) maintain referential stability when the underlying data hasn't changed.
🔬 Section 4: useMemo
for Hook Dependencies (useEffect
, useCallback
)
The principle of referential stability is also crucial when objects or arrays are included in the dependency arrays of other hooks like useEffect
and useCallback
.
4.1 - The Problem with Unstable Hook Dependencies
If useEffect
or useCallback
depends on an object or array that gets a new reference on every render, the effect will re-run or the callback will be re-created on every render, often leading to unintended consequences (like infinite loops with useEffect
if it also updates state, or negating optimizations from useCallback
).
Problematic useEffect
Example:
function DataFetcher({ options }) { // 'options' is an object { url, method }
const [data, setData] = useState(null);
useEffect(() => {
console.log("Effect running with options:", options);
// Imagine fetchLogic(options) fetches data
// fetchLogic(options).then(setData);
}, [options]); // 🚩 If 'options' is a new object on every render, this effect runs every time!
return <div>{data ? JSON.stringify(data) : "Loading..."}</div>;
}
function App() {
const [count, setCount] = useState(0);
const fetchOptions = { url: '/api/data', method: 'GET' }; // New object every render
return (
<>
<button onClick={() => setCount(c => c + 1)}>Re-render App: {count}</button>
<DataFetcher options={fetchOptions} />
</>
);
}
In the App
above, fetchOptions
is a new object on each render. So, DataFetcher
's useEffect
will run on every render of App
, potentially causing excessive API calls.
4.2 - useMemo
to Stabilize Hook Dependencies
You can use useMemo
in the parent component to stabilize the object passed as a dependency.
Solution with useMemo
for useEffect
Dependency:
// code-block-3.jsx
// Using useMemo to stabilize an object dependency for useEffect.
import React, { useState, useEffect, useMemo } from 'react';
function DataFetcher({ fetchConfig }) {
const [data, setData] = useState(null);
useEffect(() => {
console.log("Effect running with fetchConfig:", fetchConfig);
// Simulating data fetching
const fetchData = async () => {
// In a real app: const response = await fetch(fetchConfig.url, { method: fetchConfig.method });
// const result = await response.json();
// setData(result);
setData(`Data from ${fetchConfig.url} using ${fetchConfig.method}`);
};
fetchData();
// Cleanup function (example)
return ()_ => {
console.log("Cleaning up effect for:", fetchConfig);
};
}, [fetchConfig]); // ✅ Effect now only re-runs if fetchConfig (stabilized by useMemo) changes.
return <div>{data || "Loading..."}</div>;
}
function App() {
const [userId, setUserId] = useState(1);
const [forceRender, setForceRender] = useState(0); // To trigger unrelated re-renders
// Stabilize fetchConfig with useMemo
const stableFetchConfig = useMemo(() => {
console.log("Recalculating stableFetchConfig for App");
return {
url: `/api/users/${userId}`,
method: 'GET'
};
}, [userId]); // Only changes when userId changes
return (
<div>
<h2>Data Fetcher with Stable Config</h2>
<button onClick={() => setUserId(id => id + 1)}>Fetch Next User (ID: {userId + 1})</button>
<button onClick={() => setForceRender(c => c + 1)}>Force App Re-render</button>
<DataFetcher fetchConfig={stableFetchConfig} />
<p>
<em>
Open console. "Recalculating stableFetchConfig" and "Effect running..."
ONLY log when "Fetch Next User" is clicked (changing userId).
They DO NOT log when "Force App Re-render" is clicked.
</em>
</p>
</div>
);
}
export default App;
In this corrected App
:
stableFetchConfig
is created usinguseMemo
withuserId
as a dependency.- The
stableFetchConfig
object reference only changes ifuserId
changes. - Thus, the
useEffect
inDataFetcher
only re-runs its data fetching logic whenuserId
(and thereforestableFetchConfig
) actually changes, not on every unrelated re-render ofApp
.
The same principle applies if an object or array is a dependency of useCallback
.
🚀 Section 5: Advanced Considerations & useCallback
Preview
Understanding referential equality and useMemo
's role in preserving it is a cornerstone of advanced React optimization.
useMemo
for Values,useCallback
for Functions: We've focused onuseMemo
for objects and arrays (values). When you need to preserve the reference of a function (e.g., an event handler passed to a memoized child),useCallback
is the specialized hook. It works on the same principle:useCallback(fn, dependencies)
is equivalent touseMemo(() => fn, dependencies)
. We will coveruseCallback
in detail in the next articles.- The Cost of Memoization: Always remember that
useMemo
(anduseCallback
) are not free. They add a slight overhead (memory for storing the value, time for dependency comparison). Only use them when the benefit of avoiding re-renders or re-calculations outweighs this cost. This is particularly true for:- Components wrapped in
React.memo
that receive non-primitive props. - Objects/arrays used in hook dependency arrays.
- Genuinely expensive calculations (as covered in Part 1).
- Components wrapped in
- Primitives Don't Need
useMemo
for Stability: Primitive props (strings, numbers, booleans) are compared by value. You don't needuseMemo
to stabilize a primitive prop forReact.memo
.// NO need for useMemo here if passing 'count' to a memoized child
const count = 5;
// const memoizedCount = useMemo(() => 5, []); // UNNECESSARY
✨ Section 6: Best Practices and Anti-Patterns (Referential Equality Focus)
Best Practices:
- Use
useMemo
for Object/Array Props toReact.memo
Children: If a parent component re-renders frequently and passes object/array props to a child wrapped inReact.memo
, useuseMemo
in the parent to stabilize these prop references if their underlying data doesn't change.// Parent
const memoizedStyle = useMemo(() => ({ padding: 10, color: 'red' }), []);
return <MemoizedChild styleProp={memoizedStyle} />; - Use
useMemo
for Object/Array Dependencies ofuseEffect
/useCallback
: If these hooks depend on objects/arrays defined within the component body, memoize them to prevent the hooks from re-running unnecessarily.// Inside component
const options = useMemo(() => ({ threshold: 0.5 }), []);
useEffect(() => {
// ... logic using options ...
}, [options]); - Ensure Correct Dependencies for
useMemo
Itself: The dependencies ofuseMemo
should accurately reflect all reactive values used to create the memoized object/array.
Anti-Patterns (What to Avoid):
- Unnecessarily Memoizing Primitive Values for Stability: As mentioned, primitives are fine by value.
- Forgetting
useMemo
for Non-Primitive Props toReact.memo
Children: This negates the benefit ofReact.memo
if the parent re-renders often with new object/array references. - Incorrect
useMemo
Dependencies: Leading to either stale memoized values (too few dependencies) or no memoization benefit (dependencies that change on every render).// Bad: options will be new on every render, so useMemo gives no benefit here
// if user itself is a new object every render.
const options = useMemo(() => ({ id: user.id }), [user]); // if user reference changes, options changes.
// Better: const options = useMemo(() => ({ id: user.id }), [user.id]);
💡 Conclusion & Key Takeaways
You've now explored the critical role of useMemo
in managing referential equality in React. This is key to unlocking the full potential of React.memo
and ensuring stable dependencies for other hooks.
Let's summarize the key takeaways for this Part 2:
- Referential Equality Matters: Objects and arrays are compared by reference. New literals (
{}
or[]
) in a render function create new references, which can causeReact.memo
children or hooks with these dependencies to re-run. useMemo
Stabilizes References: For objects and arrays,useMemo
can return the same reference across renders if its own dependencies haven't changed.- Key Use Cases for Referential Stability:
- Passing object/array props to
React.memo
-wrapped child components. - Using objects/arrays in the dependency arrays of
useEffect
oruseCallback
.
- Passing object/array props to
useCallback
is for Functions: A specialized version ofuseMemo
for memoizing functions to achieve similar referential stability.
Challenge Yourself:
Take a component you've built that passes an options object or a style object as a prop to a child component. Wrap the child component in React.memo
. Then, use useMemo
in the parent to stabilize the object prop. Add some unrelated state to the parent to trigger re-renders and observe (via console.log
in the child) whether the child correctly avoids re-rendering when the object prop's content hasn't changed.
➡️ Next Steps
With a solid understanding of useMemo
for both expensive calculations and referential equality, you're well-equipped to write more performant React applications.
In our next article, "useCallback
for Memoizing Functions (Part 1): Preventing re-renders caused by function props.", we will focus specifically on useCallback
, the hook designed to memoize functions, often event handlers, passed down to child components.
Keep practicing these concepts. Optimizing React applications is a nuanced skill, and understanding these hooks deeply will make you a more effective developer!
glossary
- Referential Equality: Two variables holding reference types (objects, arrays, functions) are referentially equal if they point to the exact same location in memory.
obj1 === obj2
checks this. - Value Equality: Two variables have value equality if their contents are the same, even if they are different instances in memory. (e.g., two separate objects both being
{ a: 1 }
).React.memo
uses referential equality for its shallow prop comparison for objects/arrays. - Stable Reference: A reference to an object or array that does not change across multiple renders unless its underlying data (as defined by
useMemo
dependencies) changes.