React

Introduction to using React useCallback Hook

In React, there is a hook called useCallback. It wraps itself around your function and can be written like the following…

const handleClick = React.useCallback(() =>  {
  // your code goes here
  // upon re-renders, the same function instance gets returned to the child
});

As the name suggests, it is a callback function and returns a memoized version of it. What does that mean exactly? It means that the function returned from useCallback will be the same between re-renders. This is important if you have an optimized child component and you need the returned function to remain the same between re-renders, so your optimizations will actually work.

Why wouldn’t they work?

This has to do with how JavaScript evaluates certain expression types:

{} === {}  // false
[] === []  // false
const func1 = () => {};
const func2 = () => {};

func1 === func1 // true
func1 === func2 // false

So if you re-render another instance of the same function, it will be “different” in the eyes of JavaScript. This means that any optimized child components (ex: say you are using React.memo which depends on this function) will always be re-rendered… regardless of whether or not any values have changed.

Why use, useCallback?

UseCallback is mainly used for performance optimizations. That’s great, isn’t it?

Well… it really depends on the situation.

You might be thinking…

Should I use it all the time in my components??

The answer is NO!

There is always a cost involved in using such performance optimizations. This cost comes from needing additional lines of code (which means extra computations) and having to add more complexity to the code.

That’s not so good, right?

Yup!

But there is one more factor!

It’s regarding the actual performance benefit of adding useCallback into your code. If the benefit exceeds the cost, then it is a good performance optimization and vice versa if it isn’t. So before you start doing performance opitimizations, you should measure (if possible) to see if the changes will actually help, or hinder, the performance.

Then apply wisely!

Dealing with unnecessary re-renders:

Should we worry about unnecessary re-renders in our React applications? In most cases, it is not worth doing such performance optimizations. The reason for this is that our time could be better spent elsewhere (ex: writing tests, quality assurance, improving the product/service, fixing bugs, etc…).

Let’s go through a word example to help illustrate this point.

Imagine that you have a button component that is used 3 times in a parent component. So the end result will be that 3 different buttons would get rendered on the screen. Clicking on one of the buttons will either increase the button’s text value by a multiple of 1, 3 or 5.

In other words, if you click on one of the buttons this only changes the count value inside of that button. However, all three buttons would get re-rendered anyways. One could fix this “unnecessary re-render” so that only the button that is changing its value gets re-rendered by using a combination of memo & useCallback. But in most cases it is not worth adding the extra complexity to the code because the performance gain is very tiny.

However, for explanation purposes and for your understanding… let’s do it anyways!!

The final UI on the page would look like the below!

React useCallback Counter example

Let’s see the (non-optimized version) in code. With this code, anytime we click on a button, all three buttons would get re-rendered.

Inside of the child component it would look like this.

import React from "react";

const Button = ({ onClick, text }) => <button onClick={onClick}>{text}</button>;

export default Button;

Now inside of the parent component, it would look like this.

import React, { useState } from "react";
import Button from "./Button";

const Counters = () => {
  const [multipleOfOne, setMultipleOfOne] = useState(1);
  const [multipleOfThree, setMultipleOfThree] = useState(3);
  const [multipleOfSeven, setMultipleOfSeven] = useState(7);

  const handleMultipleOfOne = () => setMultipleOfOne(multipleOfOne + 1);
  const handleMultipleOfThree = () => setMultipleOfThree(multipleOfThree + 3);
  const handleMultipleOfSeven = () => setMultipleOfSeven(multipleOfSeven + 7);
  return (
    <div>
      <Button
        onClick={handleMultipleOfOne}
        text={`Click to get multiples of one: ${multipleOfOne}`}
      />
      <Button
        onClick={handleMultipleOfThree}
        text={`Click to get multiples of three: ${multipleOfThree}`}
      />
      <Button
        onClick={handleMultipleOfSeven}
        text={`Click to get multiples of seven: ${multipleOfSeven}`}
      />
    </div>
  );
};

export default Counters;

This time, let’s optimize both the child and parent components so that only the button that is clicked on gets re-rendered.

Inside of the child component it will now look like this.

import React, { memo } from "react";

const Button = memo(({ onClick, text }) => (
  <button onClick={onClick}>{text}</button>
));

export default Button;

As you can see, we simply wrap the child component in a memo and this will prevent unneccessary re-rendering behind the scenes. What exactly happens here? Memo does a check to see if the state of each button has changed, and if it hasn’t it won’t re-render the button. If something does change, it re-renders.

This is great, as it is exactly what we want!

Looking inside of the parent component, we do the following code modifications.

import React, { useCallback, useState } from "react";
import Button from "./Button";

const Counters = () => {
  const [multipleOfOne, setMultipleOfOne] = useState(1);
  const [multipleOfThree, setMultipleOfThree] = useState(3);
  const [multipleOfSeven, setMultipleOfSeven] = useState(7);

  const handleMultipleOfOne = useCallback(
    () => setMultipleOfOne(multipleOfOne + 1),
    [multipleOfOne]
  );
  const handleMultipleOfThree = useCallback(
    () => setMultipleOfThree(multipleOfThree + 3),
    [multipleOfThree]
  );
  const handleMultipleOfSeven = useCallback(
    () => setMultipleOfSeven(multipleOfSeven + 7),
    [multipleOfSeven]
  );

  return (
    <div>
      <Button
        onClick={handleMultipleOfOne}
        text={`Click to get multiples of one: ${multipleOfOne}`}
      />
      <Button
        onClick={handleMultipleOfThree}
        text={`Click to get multiples of three: ${multipleOfThree}`}
      />
      <Button
        onClick={handleMultipleOfSeven}
        text={`Click to get multiples of seven: ${multipleOfSeven}`}
      />
    </div>
  );
};

export default Counters;

Here you can see that we simply wrap each onClick handler function with useCallback. We also add the state of each multiple counter into a dependency array (ex: [multipleOfOne]). Doing this means that the state will only get updated when the actual button that corresponds to the click handler (ex: handleMultipleOfOne) is clicked on.

If we did not add the memo to the child button component, we would not see the benefits here. There would be no savings and re-renders would not get reduced.

This means that useCallback needs to be used with another optimization technique (in this case, React.memo) in order for it to work as we want it to.

Of course this is an example for illustration purposes just to see how it works. In the real world… this optimization would not be worth doing. The performance gain would be minimal, if not negative.

So where do you use it then?

I’ll explain this in the next section.

Summary Tips:

When to use, useCallback?

When you run into issues with referential equality and want to make sure that the function instance remains the same on re-renders. This is only useful in the parent component and where the child component is using an equality optimisation (Ex: React.memo). So then useCallback and React.memo work well together.

If it still isn’t making sense, go have a re-read of the beginning explanation again.

When not to use, useCallback?

If you have an expensive calculation stored as a function and needs to be used regularly (ex: on click events). Then using useMemo makes more sense, because this calculates the value once and stores it for subsequent re-uses. This means that if it has already been calculated, it won’t be calculated again.

You could also avoid using useMemo altogether by storing the value in a variable outside of the component, but you might not always have that option (ex: it is passed through as a prop).

Is it worth it?

The thing to keep in mind with performance optimizations is that they always have a cost associated to them, but may (or may not) have a large enough benefit to offset this cost. So only optimize where necessary.

David Nowak

David Nowak

If it is about business, investing, programming or travelling, you can bet he'll be interested. Known to be an easygoing guy with big ambitions and maaaybeee works too much.