DEV Community

Marko Rajević
Marko Rajević

Posted on • Edited on

Are you using useCallback properly 🤔

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>
}
Enter fullscreen mode Exit fullscreen mode

Approach without useCallback

const ParentComponent = () => {
  ...
  return <ChildComponent onClick={() => console.log('click')} />
}
Enter fullscreen mode Exit fullscreen mode

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>
})
Enter fullscreen mode Exit fullscreen mode

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 😀)

Image description

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>
  );
};
Enter fullscreen mode Exit fullscreen mode

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);
Enter fullscreen mode Exit fullscreen mode

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.

Image description

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} />
Enter fullscreen mode Exit fullscreen mode

Image description

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.

Top comments (0)