Memoization with useCallback and useMemo
Rationale
In the lifecycle of a component, React re-renders the component when an update is made. When React checks for any changes in a component, it may detect an unintended or unexpected change due to how JavaScript handles equality and shallow comparisons. This change in the React application will cause it to re-render unnecessarily.
Additionally, if that re-rendering is an expensive operation, like a long for loop, it can hurt performance. Expensive operations can be costly in either time, memory, or processing. In addition to potential technical issues, this may lead to poor user experience.
If one part re-renders, it re-renders the entire component tree. useCallback
and useMemo
can be used reduce this performance impact.
useCallback
The useCallback
hook is used to wrap functions. It tells React to not re-create a wrapped function when a component re-renders, unless any of the useCallback’s dependencies change.
useCallback
returns a memoized version of the callback function it is passed. This means that the function object returned from useCallback will be the same between re-renders.
Remember that in JavaScript, functions are objects and components are functions. As a result, every time a component containing a function re-renders, it create a new instance of the function in memory.
Given the same dependency value, the
useCallback
hook returns the same function instance between renderings (aka memoization).
This said, for small functions that are not intensive, it doesn’t really matter if they are not memoized.
Syntax
A standard case of this would be a function that runs on a button click, for instance when sending data from a form to a server. In the example below there is quite a lot going on, and most of it is independent of the actual UI-cycle of the component.
const handleSubmit = useCallback(
async (formValues) => {
setPendSaveConfig(true);
const payload = new GenerateConfig({
workflowId: project_id,
blockId: blockId,
config: formValues,
});
axios
.post(`${process.env.REACT_APP_ENDPOINT}/save-block-config`, payload)
.then((res) => console.log(res))
.finally(() => setPendSaveConfig(false))
.catch((err) => console.error(err));
},
[blockId, project_id]
);
Note that the syntax is similar to React_useEffect: there is a dependency array. The effect is the same: the function contained within useCallback
will only re-rendered if one of these dependencies changes. However (see next section) the function will run in its memoized form on every click.
Reference versus result
useCallback
only memoizes the function object (the reference) not the value that is returned by the function (the result).
In the example below, the calculatePi()
function reference will not change between renders but each time the click event fires, the value returned by calculatePi()
will change. In other words, it will be assigned fresh memory.
function ParentComponent() {
const onHandleClick = useCallback(() => {
const special = calculatePi();
});
return <SubComponent handleClick={onHandleClick} />;
}
Use cases
You should not apply useCallback
in a blanket fashion, this can reduce performance rather than improve it. The best scenarios are:
A functional component wrapped inside React.memo() accepts a function object prop
When the function object is a dependency to other hooks, e.g.
useEffect(..., [callback])
When the function has some internal state, e.g. when the function is debounced or throttled.
useMemo
We can think of useMemo
as a complement to useCallback
. The main difference is that whilst useCallback
memoizes the function reference, useMemo
memoizes the function result; the value that is returned.
In memoization, the result is “remembered” when the same parameters are passed-in subsequently. 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.
Like useCallback
, useMemo
takes a function and an array of dependencies for the same reason: if one of the dependencies changes then the function will recalculate (and re-memoize) the result but they don’t the same memoized value will be returned.
As with useCallback
, useMemo
is best applied to complex functions that are computationally expensive, it shouldn’t be used indiscriminately.
It should not be used with asynchronous functions, in these instances, useEffect
would be a better choice.
Syntax
const List = React.useMemo(
() =>
listOfItems.map((item) => ({
...item,
itemProp1: expensiveFunction(props.first),
itemProp2: anotherPriceyFunction(props.second),
})),
[listOfItems]
);