Demystifying an old React interview question: When do you use useCallback?

Masoud Bonabi
5 min readSep 29, 2023

--

Photo by Thom Holmes on Unsplash

We all have read about this hook, and know how to use it. But do we use it correctly? Do we always use it where needed and avoid it when not needed?

This is the first technical question that I ask every interviewee I interview: When do you use useCallback?

And based on the answer, I can see how deep his/her understanding is about performance on the browser.

I’m not going to state anything obvious that you find on the internet about this hook. I’m just gonna give a few hints about how you should think about the performance of a web browser.

Take a look at my Chrome dev tools:

Amount of milliseconds for a basic calculation

As you can see, doing this simple mathematical calculation for a hundred thousand times takes only 3ms on my computer (It’s not a fast one :D)

But let’s change the code inside the for loop with a simple DOM query:

Amount of milliseconds for a simple DOM query

Or another one:

Another example of the execution time for a DOM query

How about a DOM manipulation instead of a query?

The execution time of a simple DOM manipulation

The final example, bear with me:

This example makes the browser write on a file 100,000 times!

The latest example is a rare scenario of course. Not all the browser APIs have to persist a value into your filesystem.

But these examples should be enough to demonstrate how slow browser APIs work.

I’m not going to get into the technicalities of how these APIs are implemented or why they are so slow. The point I’m trying to make is that you’re website’s performance is screwed if you are using them frequently! It would directly impact the “blocking time” which is a key metric of Web Vitals. It is also a common case that the users give up on the interactivity of your website and either start rage clicking or quiting it after a while.

But Masoud, React abstracts away DOM manipulations. How do I know what manipulations are happening behind the scenes?

Excellent question! React does the abstraction using virtual DOM. How?

In simple words, there are three basic steps:

  1. It calls your function component every time a change is happened within the life cycle of your component.
  2. Your function will return something. That something will be compared to your function’s previous return value (Which is already stored by Virtual DOM).
  3. React will apply the differences between the two return values to the DOM. This is where the DOM manipulation happens.

These three steps make an algorithm that React’s dev team calls reconciliation.

So, whenever you write something like this:

the function you’re giving to theonClick prop (this one: () => setCounter(counter + 1)) will change every time your function component is called by React. Why? Because JS and React are both dumb and won’t know that the functions are the same! JS would create a new different arrow function every time your function component is called and React’s reconciliation algorithm won’t know that these two functions are the same. So it would have to do a DOM manipulation, which its simplified version would be something like this:

const buttonInsideYourHeavyComponentDomObject = react.internals.someStupidDOMUtils.findTheButtonInsideMyHeavyComponent(); // React has already cached this button's DOM object and won't run a DOM query to get to the DOM object.
const previousCallback = react.internals.virtualDOMOrSomething.findPreviousOnClickFunctionOf(buttonInsideYourHeavyComponentDomObject);
const newCallback = react.internals.blahBlah.magicallyFindTheNewCallback(buttonInsideYourHeavyComponentDomObject);

buttonInsideYourHeavyComponentDomObject.removeEventListener('click', previousCallback); // DOM manipulation happens here
buttonInsideYourHeavyComponentDomObject.addEventListener('click', newCallback); // and also here!

A simple function within a single component that won’t be reused, is not going to cause any performance problems. But if you’ve done some development with React on a real-world website, you know that a whole lot of these callback functions are needed to:

  1. make your components reusable
  2. make your website interactive

…and this is where things get nasty!

You won’t see a glitch at the first steps of your development. You probably even won’t feel it when you test the final website with your development machine. And since you are a developer, you’re likely to spend some money on a fast good mobile phone! But this is one of the random sentences that Chrome’s Lighthouse would show while auditing a website:

The average user device costs less than 200 USD.

So you shouldn’t think that your device and experience is the same as of the average user. A cheaper mobile phone means a dramatically slower CPU!

Here is another shocking statement you might already have heard of:

92.3% of internet users access the internet using a mobile phone.

This is why I always keep on asking different aspects of this hook from the interviewees; To see how deep he/she knows about the performance and web-development’s pitfalls.

A challenge to get some more perspective:

Try to find out why this code can be slow:

Just take a minute or two and try to find the issue. I know it’s not that obvious but I can guarantee that once you crack it, you’ll have a better intuition of how fragile performance can be and what you should be careful about in terms of performance and optimization.

Conclusion:

React’s docs talk about “heavy calculations” and the importance of memoization but they don’t really tell you what is considered a heavy calculation. A heavy calculation isn’t when you sort an array or when you want to fetch some data over the network. A heavy calculation mainly means DOM-related tasks. (e.g. swapping the event listener of a DOM object with another one)

This heavy calculation’s impact can be reduced by memoizing either the callback function:

const increaseCounter = useCallback(() => {
setCounter(prev => prev + 1) // setCounter's argument is an updater function.
}, []); // Try to minimize your dependencies by using updater functions.

Or by memoizing the whole component:

// You probably know the basic usage of React.memo().
// But here's another ugly weird way to achieve the same goal:

function ParentOfMyHeavyComponent() {
const MyMemoizedHeavyComponentWhichIsNotHeavyAnymore = useMemo(() => {
return (
<MyHeavyComponent />
);
}, []);

return <MyMemoizedHeavyComponentWhichIsNotHeavyAnymore />;
}

--

--