Are you using useCallback properly 🤔
Marko Rajević
Posted on March 20, 2022
I didn't up until recently.
On the project my team is working on, we used useCallback
for every function prop passed to the child components.
This approach doesn't give you benefits as you may expect.
Our code looked like this (not literally 😀)
const ParentComponent = () => {
...
const onClick = useCallback(() => console.log('click'), [])
return <ChildComponent onClick={onClick} />
}
const ChildComponent = ({ onClick }) => {
return <button onClick={onClick}>Click me</button>
}
Approach without useCallback
const ParentComponent = () => {
...
return <ChildComponent onClick={() => console.log('click')} />
}
The benefits of the first approach compared to the second one are minimal and in some cases considering the cost of useCallback
the second approach is faster.
The thing is creating and destroying functions on each rerender is not an expensive operation as you may think and replacing that with useCallback
doesn't bring much benefits.
Another reason why we always used the useCallback
hook is to prevent the child component rerender if its props didn't change but this was wrong because whenever the parent component rerenders the child component will rerender as well nevertheless the child props are changed or not.
React.memo
If you want to rerender the child component only when its props or state changed you want to use React.memo.
You can achieve the same with PureComponent or shouldComponentUpdate if you are working with class components instead of functional.
If we wrap ChildComponent from our first example with React.memo
const ChildComponent = React.memo(({ onClick }) => {
return <button onClick={onClick}>Click me</button>
})
when the ParentComponent rerenders and props of the ChildComponent don't change, the ChildComponent will not rerender.
This gives us a good insight when we should use useCallback
hook.
useCallback
should be used in combination with the React.memo
.
I will not say that should be always the case, you can use useCallback without React.memo if you find it useful but in most cases, those two should be the pair. ❤
When to use React.memo
There are no clear instructions on when to do it, someone thinks you should use it always and I'm for the approach "measure performance of your component and optimize it with React.memo
if needed".
The components which you can wrap with React.memo
by default are those with a lot of children like tables or lists.
Now we will take a look at an example.
You can clone it and try it by yourself from here https://gitlab.com/markoarsenal/use-callback.
It looks like this (very creative 😀)
We have a long list of comments (a good candidate for React.memo) and we have the counter button on the top whose main purpose is to trigger the rerender.
The code looks like this
const Home = () => {
const [counter, setCounter] = useState(0);
const onClick = useCallback(() => console.log("click"), []);
return (
<Profiler
id="Home page"
onRender={(compName, mode, actualTime, baseTime) =>
console.log(compName, mode, actualTime, baseTime)
}
>
<main className="max-w-5xl p-8 m-auto">
<div className="flex justify-center mb-8">
<button
onClick={() => setCounter(counter + 1)}
className="px-3 py-1 border border-gray-500"
>
Update {counter}
</button>
</div>
<Comments comments={comments} onClick={onClick} />
</main>
</Profiler>
);
};
You can notice Profiler
component as a root component, it's this one https://reactjs.org/docs/profiler.html.
We are using it to measure rendering times.
You can notice onRender
callback, we are logging a couple of things inside but the most important are actualTime and baseTime. The actualTime is the time needed for component rerender and baseTime is the time to rerender component without any optimizations. So if you don't have any optimizations within your component actualTime and baseTime should be equal.
Comments
component looks like this (notice that is wrapped with React.memo)
const Comments = ({ comments, onClick }: CommentsProps) => {
return (
<section>
{comments.map((comment) => {
return (
<Comment
{...comment}
className="mb-4"
onClick={onClick}
key={comment.id}
/>
);
})}
</section>
);
};
export default memo(Comments);
Now I will run our example with 500 comments in Chrome, hit the "Update" button a few times to cause rerender and post results here.
So, on every rerender we are saving around 30ms which is considerable.
Let's try one more thing, instead of the list of the comments to render one, memoized comment and see what measurements are.
{/* <Comments comments={comments} onClick={onClick} /> */}
<Comment {...comments[0]} onClick={onClick} />
Still, we have time savings but they are neglecting which means that React does not have trouble rerendering those small and simple components, and memoizing those doesn't have much sense.
On the other hand memoizing component that contains a lot of children is something you can benefit from.
Hope you enjoyed reading the article and that now you have a better overview of useCallback
and React.memo
and when to use them.
Posted on March 20, 2022
Join Our Newsletter. No Spam, Only the good stuff.
Sign up to receive the latest update from our blog.
Related
August 6, 2024