React Memoization: A complete practical guide to React.memo, useMemo and useCallback.
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
- Introduction
- Memoization
- Referential Equality and React
- React.memo
- The useMemo hook
- The useCallback hook
- React.memo, useMemo, useCallback: Complete Example
- React.memo, useMemo, useCallback: A Comparison
- Avoid Useless Memoizations
- Final Remarks
- 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:
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.
- optimization, speed up, storing: memoization is optimization of speed for the cost of space (this is part of the tradeoff I mentioned earlier).
- 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]
- cached result: expensive function call result saved nearby (let’s say the results (outputs) of our pure function given inputs (a, b))
- 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 toObject.is()
. Check out small difference in the MDN Docs. - Both
===
andObject.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 consideringReact.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 usingReact.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 touseRef
in its ability to remain unaffected by re-renders. However, their purposes differ.useRef
is used to store mutable instance variables, whileuseMemo
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:
- 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.
- Helping
React.memo
: HelpingReact.memo
memoize a component as we briefly saw above. Has to do with referential equality. - Helping hooks with dependencies: Helping another hook, with dependencies, to avoid running unnecessarily. Could be another
useMemo
, or maybe auseEffect
. - Using in place of
React.memo
anduseCallback
. - 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. UseuseMemo
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 useuseMemo
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 useuseMemo
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 auseEffect
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
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
orReact.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;