A Look At React Hooks: useCallback

A Look At React Hooks: useCallback

Introduction to the useCallback Hook

Welcome to A Look at React Hooks, a beginner-friendly series on React Hooks. In this article, let's look at the useCallback Hook.

What is useCallback?

At a glance, it seems that this Hook is pretty straightforward. It accepts 2 arguments: a callback function and an array of dependencies. It returns a memoized callback function when any value in the dependency array has changed.

In code, it looks like (from React Hooks docs):

const memoizedCallback = useCallback(
  () => {doSomething(a, b);}, //callback function
  [a, b], //dependency array
);

Memoization

The word 'memoized' or 'memoization' is an optimization technique to speed up expensive function calls by returning cached results instead of re-computing if the inputs of the function are the same.

For this Hook, understanding why we need to memoize a callback function is essential, so we know when to use it. So let illustrate that with a simple example.

An Example

Take a look at this simple app. It consists of 3 components, Todo, Number and Counter.

Capture.PNG

The Todo component displays items that were passed from props. The Number component displays the current value of number while the Counter component is responsible for the 2 buttons that can change the value of number. These 3 components are wrapped inside App.js as shown below.

function App() {
  console.log("Render App");
  //init items for Todo
  const [items, setItems] = useState([
    "1. Some todo",
    "2. Some todo",
    "3. Some todo"
  ]);
  //init number for Number and Counter
  const [number, setNumber] = useState(0);
  //add items for Todo
  const add = () => {
    setItems(() => [...items, "New todo"]);
  };
  //handler function for Counter
  const increase = () => {
    setNumber(number + 1);
  };
  //handler function for Counter
  const decrease = () => {
    setNumber(number - 1);
  };
  return (
    <div>
      <Todo items={items} add={add} />
      <Number number={number} />
      <Counter incr={increase} decr={decrease} />
    </div>
  );
}

As seen in the code, all the states and functions are within App.js while the 3 child components are merely acting as separate containers for this example.

In each component, we have a console.log statement to track when they are rendered.

In todo.js

function Todo(props) {
  console.log("Render Todo");
  return (
    <div>
      <h2>My Todo</h2>
      {props.items.map((item) => {
        return <p>{item}</p>;
      })}
      <button onClick={props.add}>Add Todo</button>
    </div>
  );
}

In number.js

function Number(props) {
  console.log("Render Number");
  return (
    <div>
      <h2>Counter</h2>
      <p>The number is: {props.number}</p>
    </div>
  );
}

In counter.js

function Counter(props) {
  console.log("Render Counter");
  return (
    <div>
      <button onClick={props.incr}>Increase</button>
      <button onClick={props.decr}>Decrease</button>
    </div>
  );
}

If we run our app, notice in the clip below that the console logs when a component is re-rendered. If we only click the Counter buttons and update the number value, the Todo component also re-renders along with Number and Counter. It is unnecessary for Todo to re-render if its values stay the same.

pure.gif

It goes for the same when an item is added to the Todo component and the items array gets updated. Both Counter and Number gets unnecessarily re-rendered even though they have nothing to do with items.

This can be an issue if a component is very huge and needs to load over a hundred items on its list. If it keeps being re-rendered even when its items do not change, it can cause performance issues in the app. There is no need to re-render the components that unrelated to a particular state update.

Solution

React.memo() is a built-in React feature that renders a memoized component and skip unnecessary re-rendering. So each component will only re-render if it detects a change in their props.

So, we can wrap the component with React.memo() in its export line:

export default React.memo(Component);

Wrap React.memo around the Todo, Number and Counter components. At this point, you might think: Great! That should be all. But let's see the app in action:

onlymemo.gif

Uh-oh! Why is it still rendering unnecessary components? When the 'Add Todo' button is clicked, we expect only App and Todo to re-render. Instead, we get App, Todo and Counter, but Number is behaving correctly.

Why is this so?

Let's go back and see how App returns the 3 components.

  return (
    <div>
      <Todo items={items} add={add} />
      <Number number={number} />
      <Counter incr={increase} decr={decrease} />
    </div>
  );

As you can see, the 3 functions written in App: add, increase and decrease are passed as props in Todo and Counter. Notice that only the value number is passed into Number as props.

In React, whenever a component re-renders, a new instance of the function in it gets generated. Therefore, every time App renders, add, increase and decrease are re-created. So their references now points to different functions in memory. Hence, in terms of referential equality, the functions before re-render are not the same as the functions after the re-render. See diagram below to visualize.

re.png

As a result, when 'Add Todo' button is clicked:

  1. App gets re-rendered.
  2. The items array and the references for add, increase and decrease gets updated.
  3. React.memo accounts for these changes and re-renders the components with items, add, increase and decrease as their props.

On the other hand, Number does not get re-rendered when 'Add Todo' button is clicked because there is no change to the number prop.

So how do we prevent the reference of the functions the same?

useCallback to the rescue

As previously mentioned, the Hook takes a callback function as its argument and a dependency array as its second. To solve the issue in our example, we simply need to wrap our handler functions in App.js: add, increase and decrease inside the Hook. This prevents the unnecessary re-rendering behaviour because it ensures the same callback function reference is returned when there is no change in their dependency.

For example, let's edit the add function first.

 //add items for Todo - before
 const add = () => {
    setItems(() => [...items, "New todo"]);
 };
 //add items for Todo - after
 const add = useCallback(() => {
    setItems(() => [...items, "New todo"]);
 }, [items])

Now, the function add will only be updated if items change. Do the same for increase and decrease functions. These functions should only be updated when number changes. They should look like:

const increase = useCallback(() => {
    setNumber(number + 1);
}, [number]);

const decrease = useCallback(() => {
    setNumber(number - 1);
}, [number]);

Result

solved.gif

Yay! Now only the relevant components will re-render. add only result in Todo updating and re-rendering. And increase or decrease will re-render Number and Counter components.

3.png

Conclusion

And that's the gist of this Hook! Thanks for reading this article. I hope it was helpful for React beginners. Please feel free to ask questions in the comments below. Ultimately, practising and building projects with this Hook will help anyone to pick it up faster.

The next Hook in this series will be: useMemo. It will be the last basic Hook in this series. After that, this series will continue for advanced/custom Hooks. Stay tuned and cheers!


Resources

Did you find this article valuable?

Support Victoria Lo by becoming a sponsor. Any amount is appreciated!

ย