If you have heard about or used the React memoization methods (useMemo, useCallback, and memo), you might often get tempted to use them in situations where you might not need them.
When I first learned about these methods, I also often ended up using them everywhere because what harm optimizing something could do, right?
Well, as you might have guessed by now, I was wrong because these hooks and methods exist for some specific use cases, and if they're used mindlessly everywhere, they can actually worsen your app's performance.
In this article, I'll try my best to explain -
- Why premature optimization is bad
- How can you optimize your code without memoizing
- When should you actually memoize
Why premature optimization is bad
You might have heard this famous quote by Donald Knuth, that "Premature optimization is the root of all evil." Well, the quote might be old, but it still holds its value for software engineers like us trying to eagerly optimize without analyzing its benefits. So let's understand why it is bad to prematurely memoize in React -
useCallback
Let's start with an example. What do you think about, handleChange in the below code snippet?
const MyForm = () => {
const [firstName, setFirstName] = React.useState('');
const handleSubmit = event => {
/**
* Omitted for brevity
*/
};
const handleChange = React.useCallback(event => {
setFirstName(event.target.value);
}, []);
return (
<form onSubmit={handleSubmit}>
<input type="text" name="firstName" onChange={handleChange} />
<button type="submit" />
</form>
);
};
I used to think that useCallback
improves performance by returning a memoized callback that only changes if one of the dependencies changes. In our case, since the dependency array is empty, it would get memoized and would be more efficient than the normal inline function, right?
But, it's not as simple as that, because every optimization comes with a cost associated with it. And in the above case, the optimization is not worth the cost it comes with. But why?
const handleChange = React.useCallback(event => {
setFirstName(event.target.value);
}, []);
In the above case, useCallback()
is called every time our MyForm
component re-renders. Even though it returns the same function object, still the inline function is created on every render, useCallback
just skips it to have the same reference to the function. Not only that, but we also have the empty dependency array, which itself is doing some work by running through some logical expressions to check if the variables inside have changed, etc.
So this is not really an optimization since the optimization costs more than not having the optimization. Also, our code is a bit more difficult to read than it was before because of the function being wrapped in a useCallback.
And as far as inline functions go, this is what the official documentation on the React website says, and they are not actually as bad as you think they are.
useMemo different yet similar
useMemo
is also very similar to useCallback
, with the only difference that it allows memoization to any value type. It does so by accepting a function that returns a value and is only recomputed when the items in the dependency list change. So again, if I didn't want to initialize something on every render, I could do this right?
const MightiestHeroes = () => {
const heroes = React.useMemo( () =>
['Iron man', 'Thor', 'Hulk'],
[]);
return (
<>
{/* Does something with heroes, Ommited for brevity */}
</>
)
}
Again the savings are so minimal that making the code more complex isn't worth it, and it's probably worse because of the same reasons, which we discussed in the previous section.
For a case like this you would be much better off by defining the array outside the component.
const heroes = ['Iron man', 'Thor', 'Hulk'];
const MightiestHeroes = () => {
// Ommited for brevity
}
Edge cases with memo
The same thing goes with memo
, if we're not careful enough your memoized component might end up doing more work and hence being more inefficient than the normal counterpart
Take this sandbox for example, how many times do you think this memoized component will render when you are incrementing the count.
But shouldn't it render only once because it takes only one children
prop which doesn't appear to be changing across renders?
Well memo
does a shallow comparison of the previous props and the new props and re-renders only when the props have changed. So if you've been working with JavaScript for some time then you must be aware of Referential Equality -
2 === 2 // true
true === true // true
'prateek' === 'prateek' // true
{} === {} // false
[] === [] // false
() => {} === () => {} // false
And since typeof children === 'object
, the equality check in memo always returns false, so whenever the parent re-renders, it will cause our memoized component to re-render as well.
How can you optimize your code without memoizing
In most cases, check if you can split the parts that change from the parts that don't change, this will probably solve most of the problems without needing to use memoization. For example, in the previous React.memo example, if we separate the heavy lifting component from the counting logic, then we can prevent the unnecessary re-renders.
You can checkout Dan Abramov's article Before you Memo if you want to read more about it.
But in some cases, you would need to use the memoization hooks and functions, so let's look at when you should use these methods.
When should you actually memoize
useCallback and useMemo
The main purpose of useCallback
is to maintain referential equality of a function when passing it to a memoized component or using it in a dependency array (since functions are not referentially equal, as discussed above). For useMemo
apart from referential equality and like memo
, it is also a way to avoid recomputing expensive calculations. Let's understand how they work with some examples -
Referential Equality
First, let's see how these hooks help us maintain referential equality, take a look at the following example (keep in mind that this is a contrived example to explain the use case of these hooks, actual implementations will vary)
const PokemonSearch = ({ weight, power, realtimeStats }) => {
const [searchquery, setSearchQuery] = React.useState('');
const filters = {
weight,
power,
searchquery,
};
const { isLoading, result } = usePokemonSearch(filters);
const updateQuery = newQuery => {
/**
* Some other stuff related to
* analytics, omitted for brevity
*/
setSearchQuery(newQuery);
};
return (
<>
<RealTimeStats stats={realtimeStats} />
<MemoizedSearch query={searchquery} updateQuery={updateQuery} />
<SearchResult data={result} isLoading={isLoading} />
</>
);
};
const usePokemonSearch = filters => {
const [isLoading, setLoading] = React.useState(false);
const [result, setResult] = React.useState(null);
React.useEffect(() => {
/**
* Fetch the pokemons using filters
* and update the loading and result state
* accordingly, omitted for brevity
*/
}, [filters]);
return { result, isLoading };
};
In this example, we have a PokemonSearch
component that uses the usePokemonSearch
custom hook to fetch the pokemons for a given set of filters. Our component receives the weight and power filters from the parent component. It also receives a prop for real-time stats, which changes quite often, as the name suggests.
Our component itself handles the last filter, called searchQuery
, via useState
. We pass this filter to a memoized component called MemoizedSearch
with a method to update it called updateQuery
.
You might have noticed by now the first problem with our example, every time our PokemonSearch
re-renders, a new reference of our updateQuery
function would be created (which would not be equal to the previous reference because of how referential equality works in JavaScript), causing the MemoizedSearch
component to re-render unnecessarily, even when the searchQuery
is same.
This is where useCallback
saves the day -
const updateQuery = React.useCallback(newQuery => {
/**
* Some other stuff related to
* analytics, omitted for brevity
*/
setSearchQuery(newQuery);
}, []);
This would help us in maintaining the same reference of the updateQuery
function which will avoid the unnecessary re-renders of our MemoizedSearch
component causing it to re-render only when the searchQuery
changes.
If you check the usePokemonSearch
custom hook, it has a useEffect
that relies on the filters
prop to decide whether to fetch the details of the pokemons whenever it changes. I hope that you noticed the next problem with our example as well. Every time the PokemonSearch
re-renders, let's suppose not due to the change in one of the filters, it creates a new reference to our filters
object, which won't be referentially equal to the last one causing the useEffect
to run with every render of PokemonSearch
and hence making a lot of unnecessary API calls.
Let's fix this with useMemo
-
const filters = React.useMemo(() => ({
weight,
power,
searchquery,
}), [weight, power, searchQuery]);
Now the filter object reference will only be updated when either of our filter changes, thus calling the useEffect
only when one of our filters change.
So the final code with all the optimizations looks like this -
const PokemonSearch = ({ weight, power, realtimeStats }) => {
const [searchquery, setSearchQuery] = React.useState('');
const filters = React.useMemo(() => ({
weight,
power,
searchquery,
}), [weight, power, searchQuery]);
const { isLoading, result } = usePokemonSearch(filters);
const updateQuery = React.useCallback(newQuery => {
/**
* Some other stuff related to
* analytics, omitted for brevity
*/
setSearchQuery(newQuery);
}, []);
return (
<>
<RealTimeStats stats={realtimeStats} />
<MemoizedSearch query={searchquery} updateQuery={updateQuery} />
<SearchResult data={result} isLoading={isLoading} />
</>
);
};
const usePokemonSearch = filters => {
const [isLoading, setLoading] = React.useState(false);
const [result, setResult] = React.useState(null);
React.useEffect(() => {
/**
* Fetch the pokemons using filters
* and update the loading and result state
* accordingly, omitted for brevity
*/
}, [filters]);
return { result, isLoading };
};
Avoiding recomputing expensive calculations
Apart from referential equality, the useMemo
hook, similar to the memo
function, serves one more purpose of avoiding recomputing expensive calculations with every render if they are not required.
For instance, take the following example, if you try to update the name really fast, you will be able to see a certain lag because the 35th Fibonacci number (which is purposefully slow and blocks the main thread while computing) is getting calculated every time your component re-renders even though the position remains the same.
Now let's try this with useMemo
. Try updating the name really fast again and see the difference -
With useMemo
we only re-calculate the Fibonacci number only when the position changes thus avoiding the unnecessary main thread work.
memo
If your component re-renders the same result given the same props, React.memo
can give you a performance boost by skipping re-rendering if the props haven't changed.
Dmitri created a really nice illustration in his article Use React.memo() Wisely which you should use a general rule of thumb when you're thinking about memoizing a component.
Enough with the concepts, let's try to understand this with an example on when React.memo
can be handy. In the below sandbox, we have a usePokemon
hook that returns some static and real-time data for a pokemon.
The static details include the name image and abilities of the Pokemon. In contrast, the real-time info includes details like the number of people who want this Pokemon and the number of people who own the Pokemon, which change quite often.
These details are rendered by three components PokemonDetails
which renders the static details, and Cravers
and Owners
, which render the real-time info, respectively.
Now, if you check the console in the above sandbox, it doesn't look good because even though PokemonDetails
consist of static data, it still re-renders every time any of our real-time values change, which is not very performant. So let's use the Checklist by Dmitri mentioned above to see if we should memoize it -
Is it a pure functional component, that given the same props renders the same output?
Yes, our
PokemonDetails
component is functional and renders the same output with the same props ✅Does it re-render often?
Yes, it re-renders often because of the realtime values provided by our custom hook ✅
Does it re-render with the same props?
Yes, the props it uses don't change at all across all its renders ✅
Is it a medium to big size component?
Since this is a very contrived example, it isn't really isn't in the sandbox, but for the sake of this example let's assume it is (Although even though isn't very expensive but given that it satisfies the above three conditions it still is a pretty good case for memoization) ✅
Since, our component satisfies the above conditions, let's memoize it -
If you check the console in the above sandbox, you'll see that it gets re-rendered only once, optimizing our code quite a bit by saving us potentially expensive re-renders.
Conclusion
If you've reached this far, I assume you get the point I am trying to make here. I'll repeat it every optimization you do comes with a cost associated with it, and the optimization is only worth it if the benefits outweigh the cost. In most cases, you might even not need to apply these methods if you can separate the parts that often change from the parts that don't change that much, as we discussed above.
I know it's a bit annoying, and maybe in the future, some really smart compiler could automatically take care of these things for you, but till then, we would have to be careful and analyze the benefits while using these optimizations.
Have I read this before?
You might have because some parts of it were inspired by this excellent post by Kent C. Dodds. I really enjoyed the post, and realized that these methods were still often misused and hence deserved more attention, so I decided to write about it with examples from some situations that I have faced.