`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
useMemofor Memoizing Expensive Calculations (Part 1). React.memofor memoizing components.- JavaScript's distinction between primitive types (strings, numbers, booleans) and reference types (objects, arrays, functions).
- Basic understanding of
useEffectanduseCallbackhooks (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. - ✅
useMemowithReact.memo: HowuseMemoenablesReact.memoto work effectively when passing object or array props. - ✅ Practical Application: Building components where
useMemoprevents unnecessary re-renders of memoized children. - ✅
useMemofor Hook Dependencies: Stabilizing object/array dependencies foruseEffectanduseCallback. - ✅ 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 itsuserConfigprop changes.AppComponent:- Has an unrelated
themestate. Toggling the theme re-rendersApp. adminUserConfig: This object is defined directly insideApp's render function. Every timeAppre-renders (e.g., due tothemechange), a brand newadminUserConfigobject is created in memory.
- Has an unrelated
- The Issue: Even though the content of
adminUserConfig(likenameandrole) might be the same across renders (unlessthemechanges the style), theUserDisplaycomponent receives a new object reference foruserConfigeach timeAppre-renders.React.memoperforms a shallow comparison:prevProps.userConfig === nextProps.userConfigwill befalsebecause they are different objects in memory. Thus,UserDisplayre-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
adminUserConfigobject is now wrapped inuseMemo. - The function provided to
useMemoreturns the configuration object. - The dependency array is
[theme]. This means the function (and thus the object creation) will only re-run if thethemestate changes.
- The creation of the
- The Result:
- When
Appre-renders butthemehas not changed,useMemoreturns the exact sameadminUserConfigobject reference as the previous render. - Now, when
React.memoinUserDisplaycomparesprevProps.userConfig === nextProps.userConfig, it will betrue(ifthemedidn't change). - Consequently,
UserDisplaywill correctly skip re-rendering if its underlying data (driven bythemein this case) hasn't changed, even ifAppre-renders for other reasons (if we had other unrelated state inApp). - In this specific example, changing the theme does change the
styleproperty withinadminUserConfig, soadminUserConfigis a new object, andUserDisplayre-renders. This is correct. Ifthemewas not a dependency for thestyle(e.g., ifbackgroundColorwas fixed), then toggling theme would not causeUserDisplayto 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
ColorSwatchcomponent, wrapped inReact.memo. It takes aconfigobject prop. - Create a
ColorSwatchListcomponent that maps over an array of color data to renderColorSwatchcomponents. - Initially, the
configobjects will be created directly in themapfunction (unstable references). - Add an unrelated state to
ColorSwatchList's parent to trigger re-renders. - Observe that all
ColorSwatchcomponents re-render. - Refactor
ColorSwatchListto 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 ifswatchConfigchanges reference.initialColorData: Our source data. InApp, we wrap it withuseMemo(() => initialColorData, [])to ensurestableColorDatahas a persistent reference acrossAppre-renders. This simulates receiving stable prop data.ColorSwatchList:- Receives
colorData. swatchConfigs = useMemo(...): This is key. We map overcolorDatato create an array ofswatchConfigobjects. This entire array of config objects is memoized. It will only be recalculated if thecolorDataprop itself changes reference.- Inside the render, we map over
swatchConfigs. EachColorSwatchreceives aswatchConfigobject from this memoized array.
- Receives
AppComponent:appRefreshCounterstate is used to forceAppto re-render without changingstableColorData.
- Optimization in Action:
- When you click "Force App Re-render":
Appre-renders.ColorSwatchListreceives the samestableColorDatareference.- Because
colorDataprop inColorSwatchListhasn't changed, theuseMemoforswatchConfigsreturns the cached array of config objects. - Each
ColorSwatchcomponent receives the sameswatchConfigobject reference as in the previous render. - Thanks to
React.memoand the stable prop references, theColorSwatchcomponents 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:
stableFetchConfigis created usinguseMemowithuserIdas a dependency.- The
stableFetchConfigobject reference only changes ifuserIdchanges. - Thus, the
useEffectinDataFetcheronly 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.
useMemofor Values,useCallbackfor Functions: We've focused onuseMemofor objects and arrays (values). When you need to preserve the reference of a function (e.g., an event handler passed to a memoized child),useCallbackis the specialized hook. It works on the same principle:useCallback(fn, dependencies)is equivalent touseMemo(() => fn, dependencies). We will coveruseCallbackin 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.memothat 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
useMemofor Stability: Primitive props (strings, numbers, booleans) are compared by value. You don't needuseMemoto 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
useMemofor Object/Array Props toReact.memoChildren: If a parent component re-renders frequently and passes object/array props to a child wrapped inReact.memo, useuseMemoin 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
useMemofor 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
useMemoItself: The dependencies ofuseMemoshould 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
useMemofor Non-Primitive Props toReact.memoChildren: This negates the benefit ofReact.memoif the parent re-renders often with new object/array references. - Incorrect
useMemoDependencies: 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.memochildren or hooks with these dependencies to re-run. useMemoStabilizes References: For objects and arrays,useMemocan 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
useEffectoruseCallback.
- Passing object/array props to
useCallbackis for Functions: A specialized version ofuseMemofor 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 === obj2checks 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.memouses 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
useMemodependencies) changes.