Now that we know the most important composition patterns and how they work, it's time to talk about performance some more. More precisely, let's discuss the topic that is strongly associated with improving performance in React, but in reality, doesn't work as we intend to at least half the time we're doing it. Memoization. Our favorite useMemo and useCallback hooks and the React.memo higher-order component. And I'm not joking or exaggerating about half the time by the way. Doing memoization properly is hard, much harder than it seems. By the end of this chapter, hopefully, you'll agree with me. Here you'll learn:
- What is the problem we're trying to solve with memoization (and it's not performance per se!).
- How useMemo and useCallback work under the hood, and what is the difference between them.
- Why memoizing props on a component by itself is an anti-pattern.
- What React.memo is, why we need it, and what are the basic rules for using it successfully.
- How to use it properly with the "elements as children" pattern.
- What is the role of useMemo in expensive calculations.
The problem: comparing values#
It's all about comparing values in JavaScript. Primitive values like strings or booleans we compare by their actual value:
const a = 1;
const b = 1;
a === b; // will be true, values are exactly the same
With objects and anything inherited from objects (like arrays or functions), it's a different story.
When we create a variable with an object const a = { id: 1 }, the value stored there is not the actual value. It's just a reference to some part of the memory that holds that object. When we create another variable with the same data const b = { id: 1 }, it will be stored in another part of memory. And since it's a different part, the reference to it will also be different.
So even if these objects look exactly the same, the values in our fresh a and b variables are different: they point to different objects in memory. As a result, a simple comparison between them will always return false:
const a = { id: 1 };
const b = { id: 1 };
a === b; // will always be false
To make the comparison of a === b return true , we need to make sure that the reference in b is exactly the same as the reference in a. Something like this:
const a = { id: 1 };
const b = a;
a === b; // will be true
This is what React has to deal with any time it needs to compare values between re-renders. It does this comparison every time we use hooks with dependencies, like in useEffect for example:
const Component = () => {
const submit = () => {};
useEffect(() => {
// call the function here
submit();
// it's declared outside of the useEffect
// so should be in the dependencies
}, [submit]);
return ...
}
In this example, the submit function is declared outside of the useEffect hook. So if I want to use it inside the hook, it should be declared as a dependency. But since submit is declared locally inside Component , it will be re-created every time Component re-renders. Remember we discussed in Chapter 2. Elements, children as props, and re-renders - a re-render is just React calling the component's functions. Every local variable during that will be re-created, exactly the same as any function in JavaScript. So React will compare submit before and after re-render in order to determine whether it should run the useEffect hook this time. The comparison will always return false since it's a new reference each time. As a result, the useEffect hook will be triggered on every re- render.
useMemo and useCallback: how they work#
In order to battle that, we need a way to preserve the reference to the submit function between re-renders. So that the comparison returns true and the hook is not triggered unnecessarily. This is where useMemo and useCallback hooks come in. Both have a similar API and serve a similar purpose: to make sure that the reference in the variable those hooks are assigned to changes only when the dependency of the hook changes. If I wrap that submit in useCallback :
const submit = useCallback(() => {
// no dependencies, reference won't change between re-renders
}, []);
}
then the value in the submit variable will be the same reference between re-renders, the comparison will return true , and the useEffect hook that depends on it won't be triggered every time:
const Component = () => {
const submit = useCallback(() => {
// submit something here
}, [])
useEffect(() => {
submit();
// submit is memoized, so useEffect won't be triggered on
every re-render
}, [submit]);
return ...
}
Exactly the same story with useMemo , only in this case, I need to return the function I want to memoize:
const submit = useMemo(() => {
return () => {
// this is out submit function - it's returned from the
function that is passed to memo
};
}, []);
As you can see, there is a slight difference in the API. useCallback accepts the function that we want to memoize as the first argument, while useMemo accepts a function and memoizes its return value. There is also a slight difference in their behavior because of that. Since both hooks accept a function as the first argument, and since we declare these functions inside a React component, that means on every re-render, this function as the first argument will always be re-created. It's your normal JavaScript, nothing to do with React. If I declare a function that accepts another function as an argument and then call it multiple times with an inline function, that inline function will be re- created from scratch with each call.
// function that accepts a function as a first argument
const func = (callback) => {
// do something with this callback here
};
// function as an argument - first call
func(() => {});
// function as an argument - second call, new function as an
argument
func(() => {});
And our hooks are just functions integrated into the React lifecycle, nothing more. So in order to return exactly the same reference in the useCallback hook, React does something like this:
let cachedCallback;
const func = (callback) => {
if (dependenciesEqual()) {
return cachedCallback;
}
cachedCallback = callback;
return callback;
};
It caches the very first function that is passed as an argument and then just returns it every time if the dependencies of the hook haven't changed. And if dependencies have changed, it updates the cache and returns the refreshed function. With useMemo , it's pretty much the same, only instead of returning the function, React calls it and returns the result:
let cachedResult;
const func = (callback) => {
if (dependenciesEqual()) {
return cachedResult;
}
cachedResult = callback();
return cachedResult;
};
The real implementation is slightly more complex, of course, but this is the basic idea. Why is all of this important? For real-world applications, it's not, other than for understanding the difference in the API. However, there is this belief that sometimes pops up here and there that useMemo is better for performance than useCallback , since useCallback re-creates the function passed to it with each re-render, and useMemo doesn't do that. As you can see, this is not true. The function in the first argument will be re-created for both of them. The only time that I can think of where it would actually matter, in theory, is when we pass as the first argument not the function itself, but a result of another function execution hardcoded inline. Basically this:
const submit = useCallback(something(), []);
In this case, the something function will be called every re-render, even though the submit reference won't change. So avoid doing expensive calculations in those functions.
Antipattern: memoizing props#
The second most popular use case for memoization hooks, after memoized values as dependencies, is passing them to props. You surely have seen code like this:
const Component = () => {
const onClick = useCallback(() => {
// do something on click
}, []);
return <button onClick={onClick}>click me</button>;
};
Unfortunately, this useCallback here is just useless. There is this widespread belief that even ChatGPT seems to hold, that memoizing props prevents components from re-rendering. But as we know already from the previous chapters, if a component re-renders, every component inside that component will also re-render.
So whether we wrap our onClick function in useCallback or not doesn't matter at all here. All we did was make React do a little more work and make our code a little harder to read. When it's just one useCallback , it doesn't look that bad. But it's never just one, is it? There will be another, then another, they will start depending on each other, and before you know it, the logic in the app is just buried under the incomprehensible and undebuggable mess of useMemo and useCallback .
There are only two major use cases where we actually need to memoize props on a component. The first one is when this prop is used as a dependency in another hook in the downstream component.
const Parent = () => {
// this needs to be memoized!
// Child uses it inside useEffect
const fetch = () => {};
return <Child onMount={fetch} />;
};
const Child = ({ onMount }) => {
useEffect(() => {
onMount();
}, [onMount]);
};
This should be self-explanatory: if a non-primitive value goes into a dependency, it should have a stable reference between re-renders, even if it comes from a chain of props.
And the second one is when a component is wrapped in React.memo
What is React.memo#
React.memo or just memo is a very useful util that React gives us. It allows us to memoize the component itself. If a component's re-render is triggered by its parent (and only then), and if this component is wrapped in React.memo , then and only then will React stop and check its props. If none of the props change, then the component will not be re- rendered, and the normal chain of re-renders will be stopped.

This is again the case when React performs that comparison we talked about at the beginning of the chapter. If even one of the props has changed, then the component wrapped in React.memo will be re- rendered as usual:
const Child = ({ data, onChange }) => {};
const ChildMemo = React.memo(Child);
const Component = () => {
// object and function declared inline
// will change with every re-render
return <ChildMemo data={{ ...some_object }} onChange={() =>
{...}} />
}
And in the case of the example above, data and onChange are declared inline, so they will change with every re-render.
This is where useMemo and useCallback shine:
const Child = ({ data, onChange }) => {};
const ChildMemo = React.memo(Child);
const Component = () => {
const data = useMemo(() => ({ ... }), []); // some object
const onChange = useCallback(() => {}, []); // some callback
// data and onChange now have stable reference
// re-renders of ChildMemo will be prevented
return <ChildMemo data={data} onChange={onChange} />
}
By memoizing data and onChange , we're preserving the reference to those objects between re-renders. Now, when React compares props on the ChildMemo component, the check will pass, and the component won't re-render.
But making sure that all props are memoized is not as easy as it sounds. We're doing it wrong in so many cases! And just one single mistake leads to broken props check, and as a result - every React.memo , useCallback , and useMemo become completely useless.
React.memo and props from props#
The first and simplest case of broken memoization is props that are passed from props. Especially when the spreading of props in components in between is involved. Imagine you have a chain of components like this:
const Child = () => {};
const ChildMemo = React.memo(Child);
const Component = (props) => {
return <ChildMemo {...props} />;
};
const ComponentInBetween = (props) => {
return <Component {...props} />;
};
const InitialComponent = (props) => {
// this one will have state and will trigger re-render of
Component
return (
<ComponentInBetween {...props} data={{ id: '1' }} />
);
};
How likely do you think that those who need to add that additional data to the InitialComponent will go through every single component inside, and deeper and deeper, to check whether any of them is wrapped in React.memo ? Especially if all of those are spread among different files and are quite complicated in implementation. Never going to happen.
But as a result, the InitialComponent breaks the memoization of the ChildMemo component since it passes a non-memoized data prop to it.
So unless you're prepared and able to enforce the rule that every single prop everywhere should be memoized, using the React.memo function on components has to follow certain rules.
Rule 1: never spread props that are coming from other components.#
Instead of this:
const Component = (props) => {
return <ChildMemo {...props} />;
};
it has to be something explicit like this:
const Component = (props) => {
return <ChildMemo some={prop.some} other={props.other} />;
};
Rule 2: avoid passing non-primitive props that are coming from other components.#
Even the explicit example like the one above is still quite fragile. If any of those props are non-memoized objects or functions, memoization will break again.
Rule 3: avoid passing non-primitive values that are coming from custom hooks.#
This seems almost contradictory to the generally accepted practice of extracting stateful logic into custom hooks. But their convenience is a double-edged sword here: they surely hide complexities away, but also hide away whether the data or functions have stable references as well. Consider this:
const Component = () => {
const { submit } = useForm();
return <ChildMemo onChange={submit} />;
};
The submit function is hidden in the useForm custom hook. And every custom hook will be triggered on every re-render. Can you tell from the code above whether it's safe to pass that submit to our ChildMemo ?
Nope, you can't. And chances are, it will look something like this:
const useForm = () => {
// lots and lots of code to control the form state
const submit = () => {
// do something on submit, like data validation
};
return {
submit,
};
};
By passing that submit function to our ChildMemo , we just broke its memoization - from now on, it will re-render as if it's not wrapped in React.memo.
See how fragile this pattern is already? It gets worse.
React.memo and children#
Let's take a look at this code:
const ChildMemo = React.memo(Child);
const Component = () => {
return (
<ChildMemo>
<div>Some text here</div>
</ChildMemo>
);
};
Seems innocent enough: a memoized component with no props, renders some div inside, right? Well, memoization is broken here again, and the React.memo wrapper is completely useless.
Remember what we discussed in Chapter 2. Elements, children as props, and re-renders? This nice nesting syntax is nothing more than syntax sugar for the children prop. I can just rewrite this code like this:
const Component = () => {
return <ChildMemo children={<div>Some text here</div>} />;
};
and it will behave exactly the same. And as we covered in Chapter 2. Elements, children as props, and re-renders, everything that is JSX is just syntax sugar for React.createElement and actually just an object. In this case, it will be an object with the type "div":
{
type: "div",
... // the rest of the stuff
}
So what we have here from a memoization and props perspective is a component that is wrapped in React.memo and has a prop with a non- memoized object in it!
In order to fix it, we need to memoize the div as well:
const Component = () => {
const content = useMemo(
() => <div>Some text here</div>,
[],
);
return <ChildMemo children={content} />;
};
or, back to the pretty syntax:
const Component = () => {
const content = useMemo(
() => <div>Some text here</div>,
[],
);
return <ChildMemo>{content}</ChildMemo>;
};
Exactly the same story applies to children as a render prop, by the way. This will be broken:
const Component = () => {
return (
<ChildMemo>{() => <div>Some text here</div>}</ChildMemo>
);
};
Our children here is a function that is re-created on every re-render. Also need to memoize it with useMemo :
const Component = () => {
const content = useMemo(
() => () => <div>Some text here</div>,
[],
);
return <ChildMemo>{content}</ChildMemo>;
};
Or just use useCallback :
const Component = () => {
const content = useCallback(
() => <div>Some text here</div>,
[],
);
return <ChildMemo>{content}</ChildMemo>;
};
Take a look at your app right now. How many of these have slipped through the cracks?
React.memo and memoized children (almost)#
If you went through your app, fixed all those patterns, and feel confident that memoization is in a good state now, don't rush. When has life ever been so easy! What do you think about this one? Is it okay or broken?
const ChildMemo = React.memo(Child);
const ParentMemo = React.memo(Parent);
const Component = () => {
return (
<ParentMemo>
<ChildMemo />
</ParentMemo>
);
};
Both of them are memoized, so it has to be okay, right? Wrong. ParentMemo will behave as if it is not wrapped in React.memo - its children are actually not memoized! Let's take a closer look at what's happening. As we already know,
Elements are just syntax sugar for React.createElement , which returns an object with the type that points to the component. If I were creating a <Parent /> Element, it would be this:
{
type: Parent,
... // the rest of the stuff
}
With memoized components, it's exactly the same. The <ParentMemo/> element will be converted into an object of a similar shape. Only the "type" property will contain information about our ParentMemo .
And this object is just an object, it's not memoized by itself. So again, from the memoization and props perspective, we have a ParentMemo component that has a children prop that contains a non-memoized object. Hence, broken memoization on ParentMemo .
To fix it, we need to memoize the object itself:
const Component = () => {
const child = useMemo(() => <ChildMemo />, []);
return <ParentMemo>{child}</ParentMemo>;
};
And then we might not even need the ChildMemo at all. Depends on its content and our intentions, of course. At least for the purpose of preventing ParentMemo from re-rendering, ChildMemo is unnecessary, and it can return back to being just a normal Child
const Component = () => {
const child = useMemo(() => <Child />, []);
return <ParentMemo>{child}</ParentMemo>;
};
useMemo and expensive calculations#
And the final, quite popular performance-related use case for useMemo is memoizing "expensive calculations." In quotes, since it's actually misused quite often as well.
First of all, what is an "expensive calculation"? Is concatenating strings expensive? Or sorting an array of 300 items? Or running a regular expression on a text of 5000 words? I don't know. And you don't. And no one knows until it's actually measured:
- on a device that is representative of your user base
- in context
- in comparison with the rest of the stuff that is happening at the same time
- in comparison with how it was before or the ideal state
Sorting an array of 300 items on my laptop, even with a 6x slowed-down CPU, takes less than 2ms. But on some old Android 2 mobile phone, it might take a second.
Executing a regular expression on a text that takes 100ms feels slow. But if it's run as a result of a button click, once in a blue moon, buried somewhere deep in the settings screen, then it's almost instant. A regular expression that takes 30ms to run seems fast enough. But if it's run on the main page on every mouse move or scroll event, it's unforgivably slow and needs to be improved.
It always depends. "Measure first" should be the default thinking when there is an urge to wrap something in useMemo because it's an "expensive calculation."
The second thing to think about is React. In particular, rendering of components in comparison to raw JavaScript calculations. More likely than not, anything that is calculated within useMemo will be an order of magnitude faster than re-rendering actual elements anyway. For example, sorting that array of 300 items on my laptop took less than 2ms. Re-rendering list elements from that array, even when they were just simple buttons with some text, took more than 20ms. If I want to improve the performance of that component, the best thing to do would be to get rid of the unnecessary re-renders of everything, not memoizing something that takes less than 2ms.
So an addition to the "measure first" rule, when it comes to memoization, should be: "don't forget to measure how long it takes to re- render component elements as well." And if you wrap every JavaScript calculation in useMemo and gain 10ms from it, but re-rendering of actual components still takes almost 200ms, then what's the point? All it does is complicate the code without any visible gain.
And finally, useMemo is only useful for re-renders. That's the whole point of it and how it works. If your component never re-renders, then useMemo just does nothing.
More than nothing, it forces React to do additional work on the initial render. Don't forget: the very first time the useMemo hook runs, when the component is first mounted, React needs to cache it. It will use a little bit of memory and computational power for that, which otherwise would be free. With just one useMemo , the impact won't be measurable, of course. But in large apps, with hundreds of them scattered everywhere, it actually can measurably slow down the initial render. It will be death by a thousand cuts in the end.
Key takeaways#
Well, that's depressing. Does all of this mean we shouldn't use memoization? Not at all. It can be a very valuable tool in our performance battle. But considering so many caveats and complexities that surround it, I would recommend using composition-based optimization techniques as much as possible first. React.memo should be the last resort when all other things have failed.
And let's remember:
- React compares objects/arrays/functions by their reference, not their value. That comparison happens in hooks' dependencies and in props of components wrapped in React.memo .
- The inline function passed as an argument to either useMemo or useCallback will be re-created on every re-render.
- useCallback memoizes that function itself, useMemo memoizes the result of its execution.
- Memoizing props on a component makes sense only when:
- This component is wrapped in React.memo .
- This component uses those props as dependencies in any of the hooks.
- This component passes those props down to other components, and they have either of the situations from above.
- If a component is wrapped in React.memo and its re-render is triggered by its parent, then React will not re-render this component if its props haven't changed. In any other case, re- render will proceed as usual.
- Memoizing all props on a component wrapped in React.memo is harder than it seems. Avoid passing non-primitive values that are coming from other props or hooks to it.
- When memoizing props, remember that "children" is also a non- primitive prop that needs to be memoized.