Skip to main content

`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 and useCallback hooks (though we'll touch upon their interaction with useMemo).

🎯 Article Outline: What You'll Master

In this article, you will learn:

  • Referential Equality Deep Dive: Understanding why {} !== {} and how this affects React.
  • useMemo with React.memo: How useMemo enables React.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 for useEffect and useCallback.
  • 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:

  1. UserDisplay: A component wrapped with React.memo. It expects to only re-render if its userConfig prop changes.
  2. App Component:
    • Has an unrelated theme state. Toggling the theme re-renders App.
    • adminUserConfig: This object is defined directly inside App's render function. Every time App re-renders (e.g., due to theme change), a brand new adminUserConfig object is created in memory.
  3. The Issue: Even though the content of adminUserConfig (like name and role) might be the same across renders (unless theme changes the style), the UserDisplay component receives a new object reference for userConfig each time App re-renders. React.memo performs a shallow comparison: prevProps.userConfig === nextProps.userConfig will be false 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:

  1. adminUserConfig = useMemo(() => { ... }, [theme]);:
    • The creation of the adminUserConfig object is now wrapped in useMemo.
    • 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 the theme state changes.
  2. The Result:
    • When App re-renders but theme has not changed, useMemo returns the exact same adminUserConfig object reference as the previous render.
    • Now, when React.memo in UserDisplay compares prevProps.userConfig === nextProps.userConfig, it will be true (if theme didn't change).
    • Consequently, UserDisplay will correctly skip re-rendering if its underlying data (driven by theme in this case) hasn't changed, even if App re-renders for other reasons (if we had other unrelated state in App).
    • In this specific example, changing the theme does change the style property within adminUserConfig, so adminUserConfig is a new object, and UserDisplay re-renders. This is correct. If theme was not a dependency for the style (e.g., if backgroundColor was fixed), then toggling theme would not cause UserDisplay to re-render.

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:

  1. Create a ColorSwatch component, wrapped in React.memo. It takes a config object prop.
  2. Create a ColorSwatchList component that maps over an array of color data to render ColorSwatch components.
  3. Initially, the config objects will be created directly in the map function (unstable references).
  4. Add an unrelated state to ColorSwatchList's parent to trigger re-renders.
  5. Observe that all ColorSwatch components re-render.
  6. Refactor ColorSwatchList to use useMemo (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:

  1. ColorSwatch: A React.memo-wrapped component. It re-renders if swatchConfig changes reference.
  2. initialColorData: Our source data. In App, we wrap it with useMemo(() => initialColorData, []) to ensure stableColorData has a persistent reference across App re-renders. This simulates receiving stable prop data.
  3. ColorSwatchList:
    • Receives colorData.
    • swatchConfigs = useMemo(...): This is key. We map over colorData to create an array of swatchConfig objects. This entire array of config objects is memoized. It will only be recalculated if the colorData prop itself changes reference.
    • Inside the render, we map over swatchConfigs. Each ColorSwatch receives a swatchConfig object from this memoized array.
  4. App Component:
    • appRefreshCounter state is used to force App to re-render without changing stableColorData.
  5. Optimization in Action:
    • When you click "Force App Re-render":
      • App re-renders.
      • ColorSwatchList receives the same stableColorData reference.
      • Because colorData prop in ColorSwatchList hasn't changed, the useMemo for swatchConfigs returns the cached array of config objects.
      • Each ColorSwatch component receives the same swatchConfig object reference as in the previous render.
      • Thanks to React.memo and the stable prop references, the ColorSwatch 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.

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 using useMemo with userId as a dependency.
  • The stableFetchConfig object reference only changes if userId changes.
  • Thus, the useEffect in DataFetcher only re-runs its data fetching logic when userId (and therefore stableFetchConfig) actually changes, not on every unrelated re-render of App.

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 on useMemo 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 to useMemo(() => fn, dependencies). We will cover useCallback in detail in the next articles.
  • The Cost of Memoization: Always remember that useMemo (and useCallback) 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).
  • Primitives Don't Need useMemo for Stability: Primitive props (strings, numbers, booleans) are compared by value. You don't need useMemo to stabilize a primitive prop for React.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 to React.memo Children: If a parent component re-renders frequently and passes object/array props to a child wrapped in React.memo, use useMemo 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 of useEffect/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 of useMemo 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 to React.memo Children: This negates the benefit of React.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 cause React.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:
    1. Passing object/array props to React.memo-wrapped child components.
    2. Using objects/arrays in the dependency arrays of useEffect or useCallback.
  • useCallback is for Functions: A specialized version of useMemo 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.

Further Reading