tldr; Don’t overuse useMemo and useCallback, Optimization comes at a cost. use
useMemo
when there is expensive calculation that only need to be re-calculate when dependencies change, not on every render. useuseCallback
when it is passed as dependency to other hooks.
Intro
useMemo
and useCallback
is all about memoization. Both accept a function
and a dependency list as arguments. useCallback
returns memoized function,
while useMemo
returns the result of running the function. Result will be
stored internally, it will be only recomputed when dependencies changes.
/* useCallback */
const memoizedFn = useCallback((a, b) => {...}, [a, b])
/* useMemo */
const memoizedValue = useMemo((a, b) => {...}, [a, b])
What is memoized function?
Upon re-render new function will created for same definition.
I’m using a global array saveHandleClick
to save the function handleClick
,
to check if function are equal between render.
// This is dummy component for demo purpose only (^_^!)
const saveHandleClick = [];
const Counter = () => {
const [counter, setCounter] = useState(0);
const handleCounterClick = () => {
setCounter(prev => prev + 1);
};
saveHandleClick.push(handleCounterClick);
return (
<>
<p>{count}</p>
<button onClick={handleCounterClick}> </button>
</>
);
}
when user clicks the button, It will re-render. Now the handleCounterClick
will be
evaluated again and new function will be created and assigned to it.
> saveHandleClick[0] === saveHandleClick[1]
> false
/* similar situation
/* same definition, but different reference
> () => {} === () => {}
If we wrap handleClick
in useCallback
It will give the previous
reference as long as the dependencies as same.
I have updated the previous example to accommodate the a new state, diff
.
const saveHandleClick = [];
const Counter = () => {
const [counter, setCounter] = useState(0);
const [diff, setDiff] = useState(1);
const handleCounterClick = useCallback(() => {
setCounter(prev => prev+diff)
},
[diff])
saveHandleClick.push(handleCounterClick);
return (
<>
<p> {diff} </p>
<button onClick={handleDiffClick}> diff </button>
<p> {counter} </p>
<button onClick={handleCounterClick}> counter </button>
</>
)
}
- Scenerio 1: clicking counter button
As the dependencydiff
haven’t changed, the function reference will be same across re-render
> saveHandleClick[0] === saveHandleClick[1]
> true
- Scenerio 2: clicking diff button
Now the dependencydiff
have changed, so function reference will change.
> saveHandleClick[1] === saveHandleClick[2]
> false
Note: if diff
haven’t added as a dependency, then function will be old version,
we will get incorrect result. But what about closure
, it should be able to access
the changes in the outer scope too, right? But on re-render new scope is created. The
saved version is refering to an older scope. Make sure that you include all the necessary
dependency for useCallback, else it will refer to older scope and may produce incorrect results.
My initial impression for useCallback
was like it saves you some space, because, we are
only storing memoizedFn
rather that actually creating a new function. But we need to
take a step back and think at what cost the memoization is happening.
But in reality new function will be created as argument for useCallback
, if the dependency haven’t changed,
old one will be returned, else new one will be returned. as a overhead, we would also need to store dependencies
to check if they have changed. So for simple function there is no point of wrapping it on useCallback
const handleClick = useCallback(() => {...}, [])
// new function will be created any way
const newFn = () => {...}
const handleClick = useCallback(newFn, [])
When to use useCallback
- When you want same instance of function across re-render, like for debounce
- if function is dependency for other hooks
What about useMemo
useMemo
is generalized version of useCallback
, it can memoize any values. If we provide a function, it will execute the function
and saves the result. Use it when you don’t want to run expensive function for every render, but when some dependencies changes.