React Memoization: A complete practical guide to React.memo, useMemo and useCallback.

hassan zaidi
24 min readOct 22, 2023

--

Disclaimer: If you’re looking for a quick reference this might not be it. I am writing this to relax and because I have nothing better to do (I’m lying, I have so much work). Go to the documentation for a quick reference. Read if you’re finally ready to understand React.memo, useMemo and the useCallback hook.

Table of Contents

  1. Introduction
  2. Memoization
  3. Referential Equality and React
  4. React.memo
  5. The useMemo hook
  6. The useCallback hook
  7. React.memo, useMemo, useCallback: Complete Example
  8. React.memo, useMemo, useCallback: A Comparison
  9. Avoid Useless Memoizations
  10. Final Remarks
  11. Appendix

TLDR: React.memo is used to prevent unnecessary re-renderings of components, while useMemo is used to prevent unnecessary computations within a component. useCallback is used to prevent unnecessary creation of functions. Reference Table 1 for a summary of what is to come.

1. Introduction

This article is part of a series I am writing to help us understand the annoying aspects of React. Honestly, this is more of a reference for myself than anything else, and if it helps someone along the way then that’s great.

In this article we talk about React.memo,useMemo, and the useCallback hook, why they are annoying, when you should use them and when you should definitely avoid them.

Unfortunately, we need to talk about memoization before we get into React’s memoization techniques. If you are even slightly unsure about memoization I suggest you read the next part, otherwise skip it like the plague.

2. Memoization [Skip if you want]

Like everything else in life, using memoization has its trade-offs. Use very little of it and you won’t realize what you’re missing, use too much of it and you’ll be worse off than from where you started.

if("you understand what you're doing" && "you don't get carried away"){
console.log("Memoization is for you");
else{
console.log("I'm not your dad, do whatever you want.")
}

Now that I have told you what you already knew, let’s discuss what this good, not so great thing is. Memoization is just caching, which itself is just a way to optimize operations. You store the result of something (expensive to compute) nearby, knowing that you’ll need it later. Something expensive to compute should probably be remembered if you know you’ll need to recalculate it.

Wikipedia has an important point to add about memoization before we wrap this redundant tirade:

“… is an optimization technique used primarily to speed up computer programs by storing the results of expensive function calls to pure functions and returning the cached result when the same inputs occur again.”

Like a 2nd grader with a highlighter I have highlighted the important parts (which is everything) above. We’ll discuss them chaotically and as I see fit.

  1. optimization, speed up, storing: memoization is optimization of speed for the cost of space (this is part of the tradeoff I mentioned earlier).
  2. pure functions: functions which given the same input/s will always give the same output/s. These functions are pure of any adulterants and/or any externalities. Why does memoization require pure functions? Because why would you store the result of a function for certain inputs, if there’s uncertainty about whether the function will produce the same output when given the same inputs again?[Remember This]
  3. cached result: expensive function call result saved nearby (let’s say the results (outputs) of our pure function given inputs (a, b))
  4. same inputs occur again: given the same inputs (a, b) the result (output) of our pure function is always what we had cached before. Why not just use it? We avoided recalculating the result, and have thereby saved time. Which is a speedup. Which is an optimisation of speed.

So memoization helps in achieving a speed up but at the cost of space (memory). We have our tradeoff. If your inputs change frequently, you’ll end up caching numerous results, which can consume memory space without providing a substantial speed boost. Therefore, memoization is most effective when inputs are either repetitive or infrequently changed. However, even if changes are frequent, as long as the inputs are often repeated, memoization remains a viable option.

Consider the scenario where your inputs are complex objects instead of simple values. In this case, you would need to perform complex comparisons each time you determine whether the current inputs have been seen before and whether their results have been cached.

Having covered the memo part in React memo, we’ll take one more tangent before finally discussing what we’re here for.

3. Referential Equality and React [Skip if you want]

As a React developer you must understand Referential Equality in JavaScript and it’s impact on React. While we are not diving deep into the concept here, you’re welcome to checkout Referential Equality and React if you’re unsure about yourself as a programmer.

The summary of what is relevant to this discussion is the following:

  • When an object in JavaScript is assigned to a variable, the variable will not hold the object itself but a reference (memory address) to the object stored in memory i.e. object stored in memory, referenced by variable.
  • This impacts equality checks for objects and what that means in JavaScript:
// two objects created with all keys,values being the same

// obj1 holds: 122336 (memory address of newly created object)
const obj1 = { name: "John", age: 30 }; // this creates new object

// obj1 holds: 889987 (memory address of another newly created object)
cosnt obj2 = { name: "John", age: 30 }; // this creates another new object

// obj3 holds: 122336 (same address as obj1)
const obj3 = obj1;

// strictly checking if both are equal
const areEqual = obj1 === obj2; // false

const areEqual2 = Object.is(obj1, obj2); // false

const areEqual3 = obj1 === obj3 // true

const areEqual4 = Object.is(obj1, obj3); // true

// also:
const areEqual5 = Object.is({},{}); // false

Takeaways from above example:

  • The === operator for this discussion and honestly all intents and purposes, is equivalent to Object.is() . Check out small difference in the MDN Docs.
  • Both === and Object.is() cannot be used to check if the actual values in objects are the same. They can only check if two variables hold reference to the same object. This is what is known as a shallow equality check.
  • A deep equality check would compare all attributes of two objects being compared, and it would be annoying to write. Fortunately, various packages provide functions for it so you don’t have to do the work in the rare instance you might need to do this.

Impact on React (we’ll focus on relevant impact)

  • Everywhere React needs to compare values it does so shallowly using Object.is(). While primitive values don’t significantly affect this process, it’s a different story for non-primitive values. We’ll see that all our memoization techniques need to do equality checks.
  • Any non-primitive value defined in the render method of a component will be re-defined whenever the component re-renders. This will cause its reference to change and for React this is change in this variable.

The following are examples of all the ways you’d define non-primitives in the render method:

// Examples of objects and arrays

// Directly inside the functional component
function MyComponent() {
const myObject = { key: 'value' };
const myArray = [1, 2, 3];
// rest of the component
}

// As a return value of a function call
function MyComponent() {
const myObject = getObject();
const myArray = getArray();
// rest of the component
}

// As a result of an operation
function MyComponent({otherObject, otherArray}) {
const myObject = { ...otherObject };
const myArray = otherArray.concat([4, 5, 6]);
// rest of the component
}

// Where getObject and getArray are defined as
function getObject() {
return { key: 'value' };
}

function getArray() {
return [1, 2, 3];
}

// Using an IIFE (Immediately Invoked Function Expression)
function MyComponent() {
const myObject = (() => ({ key: 'value' }))();
const myArray = (() => [1, 2, 3])();
// rest of the component
}

// Using a ternary operator
function MyComponent() {
const myObject = true ? { key: 'value' } : {};
const myArray = true ? [1, 2, 3] : [];
// rest of the component
}

// Using a logical operator
function MyComponent() {
const myObject = false || { key: 'value' };
const myArray = false || [1, 2, 3];
// rest of the component
}
// For functions. At the end of the day all functions are just objects
// but since the syntax is different, we'll see them separately:

// Directly inside the functional component
function MyComponent() {
function myFunction() {
// function body
}
// rest of the component
}

// As an arrow function
function MyComponent() {
const myFunction = () => {
// function body
};
// rest of the component
}

// As a return value of a function call
function MyComponent() {
const myFunction = getFunction();
// rest of the component
}

// Where getFunction is defined as
function getFunction() {
return function() {
// function body
};
}

Memoization in React, as we’ll see, will either help us avoid an unnecessary render or calculation. It (memoization techniques) will do so by checking if certain values have changed, if they have it will re-render/recalculate otherwise it will not. We would like to avoid having these values unnecessarily change because that would defeat the purpose doing the memoization.

Alright, I promise that concludes the preamble I wanted to present. We can now discuss memoization in React.

4. React.memo

Part of your job as a React developer is to minimize or root out unnecessary re-renders. These avoidable re-renders are the bane of your existence. React Memo is the blind knight you’ve gotta guide to defeat these evil folk (ensuring the knight doesn’t accidently stab you).

Amongst the many causes for unnecessary re-renders our focus is on the following: re-render of child component whenever the parent component re-renders even though props passed down to the child haven’t changed. The following example showcases this behavior:

/* This is the simplest possible example, we'll go into more detail in a sec */

/* Every time the count state in the Parent component changes,
the ChildComponent will re-render, even though its props haven't changed. */

// Parent Component
function Parent() {
const [count, setCount] = useState(0);

return (
<div>
<button onClick={() => setCount(count + 1)}>Increase Count</button>
<ChildComponent someProp="This prop doesn't change" />
</div>
);
}

// Child Component
function ChildComponent({ someProp }) {
return <div>{someProp}</div>;
}

For a detailed list of culprits for unnecessary re-renders, read the following: React and deplorable unnecessary re-renders.

React gives us React.memo to deal with this uninvited render.

What is React.Memo

React.memo is a HOC (Higher-order component).

A Higher-Order Component (HOC) in React is like a chef’s recipe that takes a basic ingredient (a component) and adds special seasonings and flavors (additional props and behavior) to create a customized dish (a new component) without altering the original recipe (component).

React.memo enables your component to avoid re-rendering when its parent re-renders, provided the child’s props remain unchanged. It’s important to view the concept of ‘unchanged’ through the lens of referential equality. This perspective will help you understand where React.memo can be effective, that is, in situations where it considers props to be unchanged.

You’d use React.memo in the following manner:

const MemoizedComponent = React.memo(SomeComponent, arePropsEqual?)

Dissecting the above is quite straightforward: React.memo is an HOC that takes in two parameters: SomeComponent and an optional param called arePropsEqual.

SomeComponent: is the component you’d like to avoid unnecessary re-renders for through memoization.

arePropsEqual (optional): a function with two arguments: your component’s old props and its new (potentially changed) props. If this function returns true, the component will not re-render (props didn’t change), otherwise it will. React.memo with arePropsEqual param specified:

const MemoizedComponent = React.memo(SomeComponent, (oldProps, newProps)=>{
// compare props here
// return true if props are equal and you want to avoid re-render
// return false if props are not equal and you want re-render
// I don't know if saying 'want' above makes sense.
})

Note: you’re not going to be using arePropsEqual (the second argument to React.memo) very often, or at least you shouldn’t be. Avoid doing complex comparisons unless you really have to i.e. avoid sending down objects as props, send simpler primitive values [Appendix]. You should also check whether this function isn’t slower than the render itself (that’d be funny).

Since React.memo memoizes components based on their props, it’s important for us to consider the kind of props we pass down to a child component and how that effects the use of React.memo.

Different Props and how they impact React.memo

The following discussion on props is why we discussed Referential equality above.

Remember: React.memo checks the equality of props shallowly i.e. it does Object.is(oldProps, newProps) to decide whether to re-render the component it is wrapped around or not.

The implications of the shallow equality check between old and new props impacts reference type props (objects, array, and, functions). Furthermore, keep in mind that React.memo is used to avoid unnecessary re-renders of child components when the parent component re-renders. However, if the prop being passed to the child is a reference type defined in the render method of the parent, the parent’s re-render will cause a re-render in the child even when the child is React.memoed. This is because the reference of the prop will change when the parent re-renders even if the value of the prop is exactly the same.

Having noted the consequences of the shallow equality let’s discuss the different scenarios of passing props to a component being React.memoed:

Primitive Props [Easy — works straightaway]

const MyComponent = React.memo(function SomeComponent(props) {
/* render using props */
return (
<div>{props.count}</div>
<div>{props.value}</div>
)
});
const ParentComponent = () => {
const [count, setCount] = useState(0);
const [value,setValue] = useState("");
return (
<button onClick={()=>setCount(prevVal=>prevVal+1)}>Increase Count</button>
<SomeComponent count={count} value={value} />
)
}

In this case, React.memo will prevent re-renders when the primitive props (like numbers, strings, booleans) are the same between renders.

Object or Array Props [annoying]

const ChildComponent = React.memo(function MyComponent({ data }) {
/* render using data */
});

const transformUserDetail = (userObject) => {
// Split the name into first and last names
const [firstName, lastName] = userObject.name.split(' ');

// Extract the domain from the email
const emailDomain = userObject.email.split('@')[1];

// Add new fields 'firstName', 'lastName', 'fullName', and 'emailDomain' to the user object
return {
...userObject,
firstName,
lastName,
fullName: `${firstName} ${lastName}`,
emailDomain,
};
};

const ChildComponent = ({name, age, email}) =>

// data object always created whenever ParentComponent renders
// no point in memoizing ChildComponent
const data = transformUserDetail({name, age, email});

return (
<SomeComponent data={data}/>
);
};

Here, React.memo will not be effective if a new array or object is created each time the parent component renders, even if the content/values is/are the same. You will need to use the second argument of React.memo, and create your own comparison function to compare the values of old and new props instead of having the default shallow comparison of React.memo. Either that or you could memoize data in the parent component using the useMemo hook to stop it’s reference from changing when parent re-renders. More on this later.

Callback Props [annoying]

const SomeComponent = React.memo(function MyComponent({ onClick }) {
/* render using onClick */

const ParentComponent = () => {
const [data, setDate] = useState({
name: 'John Doe',
age: 30,
email: 'johndoe@example.com',
});
const handleSomething ()=>{
console.log("Greetings");
}
return (
<SomeComponent onClick={handleSomething}/>
)
}

In this case, React.memo will not prevent re-renders if a new function is created each time the parent component renders. To solve this, you can use the useCallback hook in the parent component before passing the callback as a prop. You can also (kinda) do this with useMemo, but useCallback is the cleaner implementation. More on this later too.

Now that we have had the props discussions, lets discuss all considerations you need to make before using React.memo properly, efficiently and with adequate payoff.

When to and when not to consider React.memo?

Technical Considerations:

  • Does your component even receive props?
    This should be pretty obvious by now but I am putting this here for the dummies. If your component does not receive props there is no point considering React.memo.
  • Is your component unnecessarily re-rendered due to Parent re-render?
    In this scenario first consider whether the child component is worthy of saving from a re-render i.e. is it expensive to compute/render. If it is not worthy why even bother saving it?
  • Does your component have a local state and/or uses a context?
    Essentially all the things you expect React should do, it will. React.memo will not protect you from unnecessary re-renders caused by changes in context used by your component, even if the specific change does not impact the render of your component. State changes will also cause re-render which usually is what you want, except for when these changes have no impact on the rendered UI.
  • Pure rendering logic:
    We discussed in the first section that memoization is only effective when your function is pure. A functional React component is considered pure when it has the same output given that it’s context, state, props remain the same i.e. no side effects. This essentially means any use of the useEffect hook in a component disqualifies it from effective memoization using React.memo. Btw, as a reminder the following are possible side-effects: Data Fetching (API calls), Setting up Subscriptions (Event listeners, WebSockets), (Direct) DOM Manipulations, and Timers (timeouts or intervals).
// Examples of Effects in Components 

// Data Fetching
useEffect(() => {
fetch('/api/data')
.then(response => response.json())
.then(data => setData(data));
}, []);

// Subscriptions, in this case an Event Listener
useEffect(() => {
const eventHandler = () => console.log('event happened');
window.addEventListener('eventName', eventHandler);

return () => {
window.removeEventListener('eventName', eventHandler);
};
}, []);

// Direct DOM Manipulations
useEffect(() => {
inputRef.current.focus();
}, []);

// Timers: intervals and timeouts
useEffect(() => {
const timer = setTimeout(() => {
console.log('This will run after 1 second');
}, 1000);

return () => {
clearTimeout(timer);
};
}, []);

Strategic Considerations:

  • Does your component re-render often with the same props and is it expensive to render i.e. is there visible lag before the component is painted onto screen.
  • Is the nature of interactions in your app Granular or Coarse?
    The React docs (and I) want you to memoize components if interactions in your app are granular i.e. interactions that happen frequently and affect only small parts of your component tree. Coarse interactions are larger and affect a larger part of your app, therefore, even with memoization large parts of your UI will have to re-render anyway.
  • 1. Coarse Interactions — News Website: A news website where you click on a headline and the entire page changes to display the article. This is an example of a coarse interaction because the entire page (a large part of the component tree) is replaced when an interaction (clicking a headline) occurs. In this case, using React.memo might not provide significant performance benefits because large parts of the UI need to be re-rendered regardless of whether some components are memoized or not.
  • 2. Granular Interactions — Spreadsheet Application: When you update a single cell, only that cell and possibly some related cells (like those containing formulas dependent on the changed cell) need to be re-rendered. This is an example of a granular interaction because only a small part of the component tree (the individual cell components) needs to be updated in response to an interaction. In this case, using React.memo can provide significant performance benefits by preventing unnecessary re-renders of cells that haven’t changed.

Having discussed React.memo lets discuss the two hooks that let us memoize in React: useMemo and useCallback.

5. The useMemo Hook

React.memo helped us prevent some unnecessary re-renders but what if we have a legitimate/illegitimate render and an unnecessary compute intensive recalculation/recomputation of some data. Your knight in shining armour in this case is the useMemo hook.

What is the useMemo hook?

The useMemo hook is a hook used for caching results of functions to maintain their outputs between re-renders.

useMemo is akin to useRef in its ability to remain unaffected by re-renders. However, their purposes differ. useRef is used to store mutable instance variables, while useMemo is used to memoize expensive function calls to improve performance. It’s still cool to think that they aren’t that far apart in what they do.

The following is the definition of useMemo, taken from the docs:

const cachedValue = useMemo(calculateValue, dependencies)

The first argument to useMemo, calculateValue is a parameterless function that when ran, gives us the value we’re trying to cache.

The second argument, dependencies, should not be unfamiliar to you if you’ve used hooks like useEffect. This argument will take a list of all (reactive) values defined in the component (i.e. values are subject to change) that are used by (impact) the calculateValue function. The dependency array is a list of values that the calculateValue function depends on. If any of the values in the dependency array change, the calculateValue function will be rerun and its new value cached by useMemo again.

When the component first renders, the useMemoed calculateValue will run and return a value. Afterwards though, your component can re-render a 1000 times,calculateValue will only re-run if one of the defined dependencies change. In this way, you have cached the result of calculateValue and you only invalidate the cache (and therefore re-run calculateValue) when this function’s dependencies, listed in the second argument of the useMemo hook, change.

I don’t know if you saw change being italicised above, and wondered why that is. You should be asking yourself how React determines changing dependencies i.e. how old and new dependencies are compared. Surprise, surprise: React uses Object.is(). The good news is you now know the implications of such a comparison and are equipped to deal with the annoyances of referential equality.

When to and when not to consider useMemo?

The useMemo hook is the most useful and versatile of the three React tools we discuss in this article (at least imo). It can do what the other two tools do, and more (just because it can do those things, doesn’t mean you should — we’ll see later).

The React docs mention 4 cases where useMemo is exclusively effective:

  1. Computationally expensive result: avoiding recalculation of an computationally expensive result preferably one where the calculation does not depend on values that change often i.e. rarely changing dependencies.
  2. Helping React.memo: Helping React.memo memoize a component as we briefly saw above. Has to do with referential equality.
  3. Helping hooks with dependencies: Helping another hook, with dependencies, to avoid running unnecessarily. Could be another useMemo, or maybe a useEffect.
  4. Using in place of React.memo and useCallback.
  5. Computationally expensive result

First let’s discuss expensive calculations and ones that specifically fit the bill for useMemo. The following are overarching scenarios where you should consider useMemo:

  • Complex mathematical operations/Derived values from large datasets: These are operations that take a lot of time or resources to compute. Often times mathematical operations with large datasets can cause a visible lag in the UI.
    Example: function that calculates average/sum (any aggregation really) of a large array of numbers. Use useMemo to cache the function’s result and recalculate whenever the dataset changes.
// Complex mathemtical operation
// btw if you're confused factorial is an aggregation

import React, { useState, useMemo } from "react";

const Factorial = () => {
const [number, setNumber] = useState(0);

// calculateFactorial will only be rerun when number state changes
const factorial = useMemo(() => calculateFactorial(number), [number]);

const handleChange = (e) => {
setNumber(e.target.value);
};

return (
<div>
<input type="number" value={number} onChange={handleChange} />
<p>Factorial of {number} is {factorial}</p>
</div>
);
};

// complex mathematical calculation
const calculateFactorial = (num) => {
let result = 1;
for (let i = 1; i <= num; i++) {
result *= i;
}
return result;
};

export default Factorial;
  • Filter, Sort, Transformation of large array or object: You do this all the time in React, usually with filter, map, and sort methods on arrays. These operations create a new array or object from an existing one based on some criteria.
    Example: a component that displays a list of todos filtered by their status (active/completed), you can use useMemo to cache the filtered list and only recompute it when the original list or the status changes.
import React, { useState, useMemo } from "react";

/*
* Example: Component that displays a list of products filtered by
* their category and sorted by their price. Use useMemo to cache the
* filtered and sorted list and only recompute it when the original list or
* the category changes.
*/

// component receieves as prop products array
const ProductList = ({ products }) => {
const [category, setCategory] = useState("all"); // state contains a string

// if products/category change filterAndSortProducts runs again
const filteredAndSortedProducts = useMemo(() => {
return filterAndSortProducts(products, category);
}, [products, category]);

const handleChange = (e) => {
setCategory(e.target.value);
};

return (
<div>
<select value={category} onChange={handleChange}>
<option value="all">All</option>
<option value="electronics">Electronics</option>
<option value="books">Books</option>
<option value="clothes">Clothes</option>
</select>
<ul>
{filteredAndSortedProducts.map((product) => (
<li key={product.id}>
{product.name} - ${product.price}
</li>
))}
</ul>
</div>
);
};

// filters a hypothetically large array (products)
// the filtering process would take long
// doing it on every render would not make sense unless the render is
// caused by change of products (assume that products is a state in parent component which was just updated)
const filterAndSortProducts = (products, category) => {
let filteredProducts = products;
if (category !== "all") {
filteredProducts = products.filter(
(product) => product.category === category
);
}
return filteredProducts.sort((a, b) => a.price - b.price);
};

export default ProductList;
  • Creating new object from or array from existing (large) one: operations combining or modifying existing objects or arrays to create a new one.
    Example: a component that displays a list of items with their prices and discounts, you can use useMemo to cache the final price list and only recalculate it when the prices or discounts change.
import React, { useMemo } from "react";

/*
* A component that displays a list of users with their names and emails.
* You can use useMemo to cache the list of users with their emails masked
* and only recreate it when the original list changes.
*/

const UserList = ({ users }) => {
// we don't want to map users array unless the array itself changes
const maskedUsers = useMemo(() => maskEmails(users), [users]);

return (
<ul>
{maskedUsers.map((user) => (
<li key={user.id}>
{user.name} - {user.email}
</li>
))}
</ul>
);
};

const maskEmails = (users) => {
// creating new array from existing
return users.map((user) => {
const parts = user.email.split("@");
const maskedPart = parts[0].slice(0, 3) + "...";
return { ...user, email: maskedPart + "@" + parts[1] };
});
};

export default UserList;
  • Recursive functions: Recursive functions can be expensive, especially if the recursion depth is large. You could create memoized versions of recursive functions to avoid these memory hogs from running unnecessarily between renders.
// recursive function
function calculateFib(n) {
if (n === 0 || n === 1) {
return n;
}

return calculateFib(n - 1) + calculateFib(n - 2);
}

// custom hook created to do memoized fibonacci
function useMemoizedFib(n) {
const memoizedFib = useMemo(() => calculateFib(n), [n]);
return memoizedFib;
}

function MyComponent() {
const [n, setN] = useState(0);

// memoizedFib will not be recalculated unless n changes
// i.e. will remain same between renders unless n changes
const memoizedFib = useMemoizedFib(n);

// calculateFib re-runs on each render
const fib = calculateFib(n);

return (
<div>
<input
type="number"
value={n}
onChange={(e) => setN(e.target.value)}
/>
<p>The {n}th Fibonacci number is {memoizedFib}</p>
</div>
);
}

Strategic Considerations:

  • A simple eye test is the easiest way to sniff out complex calculations or a potentially large dataset (array/object). If you see it lag, consider memoization.
  • Generally, you should consider a dataset large for React rendering when it has values in the thousands.
  • Again if dependencies change way too often, it wouldn’t make a lot sense to use useMemo there.

2. useMemo with React.memo

Memoization of component with React.memo will be ineffective when it is passed a prop from parent component that changes on every render. This could be a primitive value but more often than not it is a non-primitive defined in the render method of the parent component. The useMemo hook makes the object not change (in terms of reference) between renders thereby rescuing the React.memo memoization.

3. useMemo for hooks with dependencies

React hooks useEffect and useMemo accept dependency arrays, as we have seen in the beginning of our discussion on useMemo. This dependency array controls whether the hook should re-run or not based on whether the values inside dependency array have changed or not. The problem we face again would be with non-primitives defined in render methods used as values in dependency array. Similar to how useMemo rescued React.memo, it can rescue itself and useEffect.

4. Using in place of React.memo and useCallback.

You can potentially use useMemo in place of React.memo to memoize components but you probably shouldn’t because it’s messier and isn’t designed for it.

You can definitely use useMemo in place of useCallback. Let’s explore how that is, and why you should still use the useCallback hook.

6. The useCallback hook

The lamest of the three but potentially the most used, useCallback is used to memoize function definitions. Important to note that unlike useMemo, useCallback does not store the result of when the function is called but the function definition itself.

We need to memoize functions in React to prevent unnecessary re-creations during re-renders, especially when they’re hook dependencies or props for memoized components.

First let’s see how useMemo would handle caching a function:

import React, { useState, useMemo, memo } from 'react';

// Memoized Child component takes in memoized callback as prop
const Child = memo(({ increment }) => {
return <button onClick={increment}>Increment</button>;
});

function App() {
const [count, setCount] = useState(0);

// useMemo caches setCount function
// function only recreated when count changes
const increment = useMemo(() => {
return () => setCount(count + 1);
}, [count]);

return (
<div>
<p>Count: {count}</p>
{/*Memoized callback function passed*/}
<Child increment={increment} />
</div>
);
}

export default App;

We see that the function passed to useMemo needs to return our original callback in order for the callback definition to be cached.

The same with useCallback would look like the following:

import React, { useState, useCallback, memo } from 'react';

// Memoized Child component takes in memoized callback as prop
const Child = memo(({ increment }) => {
return <button onClick={increment}>Increment</button>;
});

function App() {
const [count, setCount] = useState(0);

// useCallback makes the same code a bit easier to read and less clunky.
const increment = useCallback(() => {
setCount(count + 1);
}, [count]);


return (
<div>
<p>Count: {count}</p>
{/*Memoized callback function passed*/}
<Child increment={increment} />
</div>
);
}

export default App;

Therefore useCallback is basically: (from React docs)

// Simplified implementation (inside React)
function useCallback(fn, dependencies) {
return useMemo(() => fn, dependencies);
}

As you can see, useCallback helps us avoid writing clunky code by removing that extra line required for useMemo to memoize a function definition.

The useCallback hook has a pretty singular aim in what it can do. It is simply useMemo for function definitions. Pass it to a memoized component like you’d pass an object.

Some things to maybe keep in mind (mentioned very clearly in the docs) are:

  • For state setters using previous state in useCallback, omit the state from dependencies.
const [state, setState] = useState(initialState);

// do this
const memoizedSetter = useCallback((value) => {
setState(prevState => prevState + value);
}, []);

// don't do this, you can, but don't
const memoizedSetter = useCallback((value) => {
setState(state + value);
}, [state]);
  • Memoizing a function with useCallback optimizes a useEffect hook that depends on it, preventing unnecessary re-renders.
const memoizedFunction = useCallback(() => {
// function body
}, []);

useEffect(() => {
memoizedFunction();
}, [memoizedFunction]);
  • In a custom hook that returns functions, always memoize these functions using useCallback so that other developers can easily use these functions with their other memoizations.

7. React.memo, useMemo, useCallback: Complete Example

The following is an example of complete memoization. You can tell the all techniques of memoization go hand in hand in React.

  • React.memo used for memoization of component where props probably won’t change a lot.
  • useMemo used to memoize expensive calculation of balance and selectedTransaction. These are expensive because transaction array is massive. Even if it wasn’t, this would still not be a bad idea.
  • useCallback used to memoize function definition that is being passed to React.memoed component called Transaction.
import React, { 
useState,
useEffect,
useMemo,
useCallback,
memo
} from 'react';

// React.memo to memoize component
const Transaction = memo(({ transaction, onSelect, selected }) => {
const style = selected ? { backgroundColor: 'lightgray' } : {};
return (
<div onClick={() => onSelect(transaction.id)} style={style}>
<p>{transaction.description}: ${transaction.amount}</p>
</div>
);
});


const TransactionList = ({ transactions, onSelect, selectedTransaction }) => {
return (
<div>
{transactions.map(transaction => (
<Transaction
key={transaction.id}
transaction={transaction}
onSelect={onSelect}
selected={selectedTransaction && (
transaction.id === selectedTransaction.id
)}
/>
))}
</div>
);
};

function App() {
const [userId, setUserId] = useState(1);

// stores large number of transactions - in the 1000s
const [transactions, setTransactions] = useState([]);
const [selectedTransactionId, setSelectedTransactionId] = useState(null);


// We're using useEffect to fetch transactions from the API
useEffect(() => {
const apiEndpoint = `https://api.example.com/users/${userId}/transactions`;
fetch(apiEndpoint)
.then(response => response.json())
.then(data => setTransactions(data))
.catch(error => console.error('Error:', error));
}, [userId]);

// expensive calculation memoized using useMemo
const balance = useMemo(() => {
return transactions.reduce((total, transaction) => total + transaction.amount, 0);
}, [transactions]);

// memoizing variable passed as prop
const selectedTransaction = useMemo(() => {
return transactions.find(transaction => transaction.id === selectedTransactionId);
}, [transactions, selectedTransactionId]);

// memoizing function definition using useCallback
// is needed for function memoized using React.memo
const handleSelect = useCallback((id) => {
setSelectedTransactionId(id);
}, []);

return (
<div>
<p>Total Balance: ${balance}</p>
<TransactionList
transactions={transactions}
onSelect={handleSelect}
selectedTransaction={selectedTransaction}
/>
{selectedTransaction && (
<div>
<h2>Transaction Details</h2>
<p>{selectedTransaction.description}: ${selectedTransaction.amount}</p>
</div>
)}
</div>
);
}

export default App;

8. React.memo, useMemo, useCallback: A Comparison

Table 1: Summary of React.memo vs the useMemo Hook

9. Avoiding Useless Memoizations

We have already seen how non-primitive dependencies/props can nullify the memoization you’ve done.

The chief concern to remember to avoid useless memoizations:

Beware of functions, objects and arrays created, defined inside the render method of a component. Don’t depend on them for re-rendering or recalculations i.e. don’t (directly) use them in dependency array of hooks and don’t pass them as props to React.memoed components.

Don’t memoize to make yourself feel better and the code worse (both in terms of speed and readability).

The biggest cause for useless memoizations is a tenuous grasp of Referential equality and it’s role in React. By now, you should have a firm grasp of it, or your path ahead will be treacherous and your attempts to cross it futile.

10. Final Remarks

  • For all the fear of incorrect/unnecessary memoization I have instilled in you, memoizing without a care in the world is not that bad. You probably won’t have visible adverse effects of using useMemo or React.memo imprudently. But it does make your code uglier and harder to follow. It is also not a bad idea to build better coding habits so that the people around you aren’t counting the days till you leave the team.
  • Do not use React.memo with components that are quick and easy to render, performance improvement will not be noticeable or might actually be worse (overall) due to memoization overheads. Again not that big a deal if you do use it.
  • Save React.memo for after you’ve created your component in its entirety. While you’ll undoubtedly spot opportunities for memoization, this way you’ll reduce bugs and spot if a bottleneck is lurking. Debugging with memoization can be extremely annoying and you’d only do it if you truly hate yourself.
  • For the love of God don’t do useless memoizations.
  • Don’t use memoization techniques as band-aid to solve problems that have root causes elsewhere.
  • Oh and btw, after all this, your components might still unnecessarily re-render because (from the docs): “memoization is a performance optimization, not a guarantee”. Don’t rely on it as the basis of your component’s logic or UI.

11. Appendix

1. Avoid Sending Objects as Props when using React.memo

Avoid sending entire objects down as props, instead, it’s a good practice to extract the individual values from the complex object and send them as separate props.

// Don't do this

import React from 'react';

// Complex object as prop
const ComplexObjectComponent = React.memo(({ user }) => {
return (
<div>
<p>Name: {user.name}</p>
<p>Age: {user.age}</p>
<p>Email: {user.email}</p>
</div>
);
});

const App = () => {
const userObject = {
name: 'John Doe',
age: 30,
email: 'johndoe@example.com',
};

return (
<div>
<h1>User Details</h1>
{/* Sending the complex object as prop */}
<ComplexObjectComponent user={userObject} />
</div>
);
};

export default App;

--

--