You may have seen, released along with React Hooks, a strange hook called useMemo
. What could this strange, ๐ฝalien๐ฝ hook mean and what is it used for? Most importantly, how can it help you and why do you need to know about it?
First, a little brush up on JavaScript equality.
Referential Equality
You may remember how Javascript compares objects ๐ฅด. There are some tricky results when we run equality comparisons:
{} === {} // falseconst z = {}
z === z // true
React uses Object.is
to compare components, but that gives very similar results to using ===
. So, when React checks for any changes in a component, it may find a โchangeโ that we wouldnโt really consider a change.
() => {} === () => {} // false
[] === [] // false
This comparison check will cause some React re-rendering we didnโt intend or expect. If that re-rendering is some expensive operation, that can hurt performance. If one part re-renders, it re-render the entire component tree. Thus, React released the memo idea to fix this.
Memoization
You may have heard this fancy word, memoization. Memoization is basically an optimization technique which passes a complex function to be memoized or remembered. In memoization, the result is remembered, when the same exact parameters are passed-in subsequently. Kind of like memorization. If we have a function compute 1 + 1, it will return 2. But if it uses memoization, the next time we run 1โs through the function it wonโt add them up, it will just remember the answer is 2 without executing the adding function.
In React, memoization optimizes our components, avoiding complex re-rendering when it isnโt intended. Here is a great article on using React.memo to optimize your application. React.memo acts like a pure component, wrapping your component and the linked article does a great job explaining it. However, useMemo is a bit different in usage.
From the official React documentation, useMemoโs signature looks like this:
const memoizedValue = useMemo(() => computeExpensiveValue(a, b), [a, b]);
useMemo
const memoizedValue = useMemo(() => computeExpensiveValue(a, b), [a, b]);
Returns a memoized value.
Pass a โcreateโ function and an array of dependencies. useMemo will only recompute the memoized value when one of the dependencies has changed. This optimization helps to avoid expensive calculations on every render.
Remember that the function passed to useMemo runs during rendering. Donโt do anything there that you wouldnโt normally do while rendering. For example, side effects belong in useEffect, not useMemo.
As you can see, useMemo
takes in a function and a list of dependencies (the array [a, b]
). If you are familiar with useEffect, then you have probably seen the dependencies idea. They act similar to arguments in a function. The dependencies list are the elements useMemo watches: if there are no changes the function result will stay the same, otherwise it will re-run the function. If they donโt change, it doesnโt matter if our entire component re-renders, the function wonโt re-run but instead return the stored result. This can be optimal if the wrapped function is large and expensive. That is the primary use for useMemo.
useMemo Example
const List = useMemo(
() =>
listOfItems.map(item => ({
...item,
itemProp1: expensiveFunction(props.first),
itemProp2: anotherPriceyFunction(props.second)
})),
[listOfItems]
)
In the above example, the useMemo function would run on the first render. It would block the thread until the expensive functions complete, as useMemo
runs in the render. Initially, this wonโt look as clean as useEffect, since useEffect can render a loading spinner until the expensive functions finish and the effects fire off. However, the expensive functions would never fire off again if listOfItems
never changed and we would still get the return value from them. It would make these expensive functions seem instantaneous. This is ideal of you have an expensive, synchronous function or two.
Prevent Re-Rendering
If youโre familiar with Reactโs class-component lifecycle hook, shouldComponentUpdate, useMemo
has a similar usage in preventing unnecessary re-renders. Letโs say we have this instance:
function BabyGator({fish, insects}) { // because what else do baby gators eat?
const dinner = {fish, insects};
useEffect(() => {
eatFunction(dinner);
}, [fish, insects])
}function MamaGator() {
return <BabyGator fish='small edible fish' insects={15}>
}
This works perfectly well. Our useEffect
hook watches the fish and insects props being passed-in (he is hungry). However, this only works with primitive values. Thatโs the key.
Remember back to talking about Referential Equality? [] === [] // false
. Thatโs exactly what memoizing hooks like useMemo
and useCallback
are made for. If our insects prop is an array, we can put it in the useMemo
hook and it will reference it, after a render, as equal. If a function or another non-primitive value is in the useEffect
dependency, it will recreate a new array, because of closure, and find it unequal.
Obviously here, we donโt need useMemo
, if all we are doing is memoizing an array. But if there was an expensive function to calculate that array, useMemo
would be useful!
useCallback
const memoizedCallback = useCallback(
() => {
doSomething(a, b);
},
[a, b],
);
Returns a memoized callback.
Pass an inline callback and an array of dependencies. useCallback will return a memoized version of the callback that only changes if one of the dependencies has changed. This is useful when passing callbacks to optimized child components that rely on reference equality to prevent unnecessary renders (e.g. shouldComponentUpdate).
useCallback(fn, deps) is equivalent to useMemo(() => fn, deps).
When Not to useMemo
The useCallback
hook is similar to useMemo
, but it returns a memoized function, while useMemo
has a function that returns a value. If our dependencies array is empty, there is no possibility of memoization and it will compute a new value on every render. You might as well implement the useRef
hook at that point. The advantage useMemo
offers over useRef
is a re-memoizing if the dependencies change.
When looking to implement useMemo
always ask, โis this really an expensive function?โ Expensive means it is sucking up a lot of resources (like memory). If you are defining a ton of variables in a function at render, it might make sense to memoize with useMemo
.
You wonโt want to have useMemo
fire off any side effects or any asynchronous calls. Both of those would make more sense to be contained within useEffect
.
When looking to implement useMemo
, write the code first then revisit it to see if you can optimize it. Donโt start with useMemo
. Too many people implement it quickly and it can make performance worse in a small application.