Introduction

I recently started learning about the React hooks API and I was amazed by how expressive it is. Hooks allow me to rewrite tens of lines of boilerplate code with just a few lines. Unfortunately, this convenience comes at a cost. I found that some more advanced hooks like useCallback and useMemo are hard to learn and appear counter-intuitive at first.

In this article, I’ll demonstrate with a few simple examples why we need these hooks and when and how to use them. This is not an introduction to hooks, and you must be familiar with the useState hook to follow.

The Problem

Before we start, let’s introduce a helper button component. We’ll use React.memo to turn it into a memoized component. This will force React to never re-render it, unless some of its properties change. We’ll also add a random colour as its background so we can track when it re-rerenders:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
import React, { useState, useCallback } from 'react';

// Generates random colours any time it's called
const randomColour = () => '#'+(Math.random()*0xFFFFFF<<0).toString(16);

// The type of the props
type ButtonProps = React.ButtonHTMLAttributes<HTMLButtonElement>;

// A memoized button with a random background colour
const Button = React.memo((props: ButtonProps) => 
  <button onClick={props.onClick} style={{background: randomColour()}}> 
    {props.children}
  </button>
)

Now let’s look at the following simple app. It displays 2 numbers - a counter c and a delta. One button allows the user to increment delta by 1. A second button, allows the user to increment the counter by adding delta to it. We’ll create 2 functions increment and incrementDelta and assign them to the buttons’ on-click event handlers. Let’s also keep track of how many such functions are created while the user clicks the buttons:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
import React, { useState } from 'react';

// Keeps track of all created functions during the app's life 
const functions: Set<any> = new Set();

const App = () => {
  const [delta, setDelta] = useState(1);
  const [c, setC] = useState(0);

  const incrementDelta = () => setDelta(delta => delta + 1);
  const increment = () => setC(c => c + delta);

  // Register the functions so we can count them
  functions.add(incrementDelta);
  functions.add(increment);

  return (<div>
    <div> Delta is {delta} </div>
    <div> Counter is {c} </div>
    <br/>
    <div>
      <Button onClick={incrementDelta}>Increment Delta</Button>
      <Button onClick={increment}>Increment Counter</Button>
    </div>
    <br/>
    <div> Newly Created Functions: {functions.size - 2} </div>
  </div>)
}

When we run the app and start clicking the buttons we observe something interesting. For every click of a button there are 2 newly created functions! Futhermore, both buttons re-render on every change!

Without useCallback
For every re-render of the component, 2 new functions are created. Every change causes both buttoms to re-render.

In other words, at every re-render we’re creating 2 new functions. If we increment c, why do we need to recreate the incrementDelta function? This is not just about memory - it causes child components to re-render unnecessarily. This can quickly become a performance issue.

One solution would be to move the two functions outside of the the App functional component. Unfortunately, this wouldn’t work because they use the state variables from App’s scope.

Naive solution - Why dependencies matter

This is where the useCallback hook comes in. It takes as an arguement a function and returns a cached/memoized version of it. It also takes a second parameter which will cover later. Let’s rewrite with useCallBack:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
const App = () => {
  const [delta, setDelta] = useState(1);
  const [c, setC] = useState(0);

  // No dependencies (i.e. []) for now
  const incrementDelta = useCallback(() => setDelta(delta => delta + 1), []);
  const increment = useCallback(() => setC(c => c + delta), []);

  // Register the functions so we can count them
  functions.add(incrementDelta);
  functions.add(increment);

  return (<div>
    <div> Delta is {delta} </div>
    <div> Counter is {c} </div>
    <br/>
    <div>
      <Button onClick={incrementDelta}>Increment Delta</Button>
      <Button onClick={increment}>Increment Counter</Button>
    </div>
    <br/>
    <div> Newly Created Functions: {functions.size - 2} </div>
  </div>)
}

This prevents the instantiation of new functions and unnecessary re-renders. However, when we re-run the app, we notice that we’ve introduced a bug. If we increment detla to 2, and then try to increment the counter, its value increases by 1, not by 2:

Without dependencies
No new functions are created regardless of delta's state changes. During the initial rendering, `useCallback` created a single cached version of the "increment" function, which encapsulate the detla state value and reused it on every re-render.

This is because at the point of instantiation of the increment function, the value of delta was 1, and this was captured in the function’s scope. Since we’re caching the increment instance, it’s never recreated and it uses its original scope with detla = 1.

The useCallback hook has created a single cached version of increment, which encapsulates the initial value of delta. When App re-renders with different values for delta, useCallback returns the previous version of the increment function which keeps the old value of delta from the first rendering.

We need to tell useCallback to create new cached version of increment for every change of delta.

Dependencies

This is where the second arguement of useCallback comes in. It is an array of values, which represents the dependencies of the cache. On any two subsequent re-renders, useCallback will return the same cached function instance if the values of the dependencies are equal.

We can use dependencies to solve the previous bug:

1
2
3
4
  const incrementDelta = useCallback(() => setDelta(delta => delta + 1), []);

  // Recreate increment on every change of delta!
  const increment = useCallback(() => setC(c => c + delta), [delta]);

Now we can see that a new increment function is created only when delta changes. Therefore, the counter button only re-renders when delta changes, because a new instance of the onClick property is added. In other words, we only create a new callback, if the part of the closure it uses (i.e. the dependencies) has changed since the previous rendering.

With dependencies
A new `increment` function is created on every change of `delta`. Only the function whose dependencies change is recreated.

A really useful feature of useCallback is that it returns the same function instance if the depencies don’t change. Hence we can use it in the dependecy lists of other hooks. For example, let’s create a cached/memoized function which increments both numbers:

1
2
3
4
5
6
7
8
const incrementDelta = useCallback(() => setDelta(delta => delta + 1), []);
const increment = useCallback(() => setC(c => c + delta), [delta]);

// Can depend on [delta] instead, but it would be brittle
const incrementBoth = useCallback(() => {
    incrementDelta();
    increment();
}, [increment, incrementDelta]); 

The new incrementBoth function transitively depends on delta. We could write useCallback(... ,[delta]) and that would work. However, this is a very brittle approach! If we changed the dependencies of increment or incrementDelta, we would have to remember to change the dependencies of incrementBoth.

Since the references of increment and incrementDelta won’t change unless their dependencies change, we could use them instead. Transitive dependencies can be ignored! This makes for a straightforward rule:

Each function declared within a functional component’s scope must be memoized/cached with useCallback. If it references functions or other variables from the component scope it should list them in its dependency list.

This rule can be enforced by a linter which checks that your useCallback cache dependenices are consistent.

Two similar hooks - useCallback and useMemo

React introduces another similar hook called useMemo. It has similar signature, but works differently. Unlike useCallback, which caches the provided function instance, useMemo invokes the provided function and caches its result.

In other words useMemo caches a computed value. This is usefull when the computation requires significant resources and we don’t want to repeat it on every re-render, as in this example:

1
2
3
4
5
const [c, setC] = useState(0);

// This value will not be recomputed between re-renders
// unless the value of c changes
const sinOfC: number = useMemo(() => Math.sin(c) , [c])

Just as with useCallback, the values returned by useMemo can be used as other hooks’ dependencies.

As an interesting aside, useMemo can cache a function value too. In other words, it is a generalised version of useCallback and can replace it as in the following example

1
2
3
4
5
6
// Some function ...
const f = () => { ... }

// The following are functionally equivalent
const callbackF = useCallback(f, [])
const callbackF = useMemo(() => f, [])