Not long ago, I spent longer than I expected time on refactoring a piece of code that ends up utilising react.useMemo()
. I blamed it on my false assumption that I understood react.useMemo()
and react.useCallback()
well enough, which apparently was not the case. So in this blog, I will take another look at the two hooks again, with some well-explained distinctions and well-supplied examples.
So what are useMemo
and useCallback
hook used for? Well it’s easy to say that useMemo
is to memoize a value between renders and useCallback
is to memoize a function call between renders.
Referential equality
Still sounds confusing? So am I! So let’s talk about referential equality in React first.
In JavaScript it’s easy to compare Primitive values to each other, like booleans, numbers, and strings. But when it comes to objects, we often see:
{} === {} // false
{"a"} === {"a"} // false
[] === [] // false
() => {} === () => {} // false
This is because for an object to equal another object, we need to make sure both object have same reference.
In React, whenever we define an object in the component, it won’t be the same one (referentially equal) to the last time itself when the component re-renders.
This can be annoying especially with computationally expensive calculations.
You may say this can potentially be fixed with useEffect
:
const Bar = ({bar})=> {
const deps = {bar}
useEffect(() => {
expensive_calc(deps)
}, [deps])return <></>
}const Foo = () =>
const bar = 3
<Bar bar={bar}/>
We can see whenever bar gets passed from Foo
to Bar
the expensive_calc
inside the useEffect
hook needs to re-do the calculation. That’s because useEffect
is going to do a referential equality check on deps
whenever it renders, and it will always evaluate to true. But we want to calculate again if the bar
value changes.
If bar
is a primitive value, we can avoid this happening by changing the depend list:
useEffect(() => {
expensive_calc(deps)
}, [bar])
But what if it is not? Like an array, a function or a nested object? We can use useCallback
and useMemo
.
const Foo = () => {
const bar = useCallback(() => 3, [])
// const bar = useMemo(() => {()=>3}, [])
// rewrite using useMemo instead<Bar bar={bar} />
}
Now the expensive_cal
will only change when bar
value changes thanks to the memoized value.
A close comparison
You can also see that the signature of useMemo
and useCallback
is very similar, and that they can convert to each other. They both take in a function and an array of dependencies.
useCallback(fn, deps);
useMemo(fn, deps);
To convert to each other:
useCallback(fn, deps) == useMemo(() => fn, deps)
Still, difference exist in that useMemo
gives the referential equality between renders for values, and useCallback
gives referential equality between renders for functions. useCallback
returns the function uncalled while useMemo
calls the function and gets the value for you.
const foo = () => "foo"const memoizedCb = useCallback(foo, []);
const memoizedMm = useMemo(foo, []);memoizedCb(); // 'foo'
memoizedMm; // 'foo'
In a way,useCallback
is just a special use case of useMemo
, as the former can only takes a function, but the latter can take any value including functions.
Use cases
So what are the use cases for both hooks and what are the ones best suited for each?
In general, they are both suited whenever there’s checking for referential equality.
But there are slight differences. useMemo
for really heavy calculations — e.g. heavy filtering, recursive calculations.
In case of lazy loading, both can do the same purpose. In the example below, the Child
component will skip rendering if argument child
remains the same.
function Parent(child) {
const child = useMemo(() => <Child item={child} />, [child]);
return (
<>
{child}
</>
)
}
For useCallback
, it more focuses on inline function call like onClick={() => { doSomething(...); }.
For example the component Child below will gets re-rendered each time the parent is re-rendered — because the inline functiondoSomethingInLine
is recreated with a different reference each time:
function Parent({ ... }) {
const [child, setChild] = useState(0);
return (
<Child onChange={() => { doSomethingInLine(child); }} />
);
}
To adduseCallback
:
function Parent({ ... }) {
const [child, setChild] = useState(0);
const addCb = useCallback(() => {doSomethingInLine(child);}, [child]);
...
return (
...
<Child onChange={addCb} />
);
}
So you can see if you have a child component that takes a function prop from its parent and with every re-render of its parent, the child need to re-render due to the recreation of its function prop. You can fix it by wrapping the function prop in a useCallback
hook. So now the child will only rerender when there is a dependency change.
Note that the performance boost is not free. They ALWAYS come with a price for React to save the result/callback to the memory and do a comparison each time.
And if you do need to use them. Try not to omit the dependency list argument as otherwise the value will never be memoized.
That’s so much of it today!
Happy Reading!