It’s no secret that React.js has become widely popular in recent years. It’s now the JavaScript library of choice for many of the internet’s most prominent players, including Facebook and WhatsApp.

One of the main reasons for its rise was the introduction of hooks in version 16.8. React hooks allow you to tap into React functionality without writing class components. Now functional components with hooks have become developers’ go-to structure for working with React.

In this blog post, we’ll dig deeper into one specific hook — useCallback — because it touches on a fundamental part of functional programming known as memoization. You’ll know exactly how and when to utilize the useCallback hook and make the best of its performance-enhancing capabilities.

Ready? Let’s dive in!

What Is Memoization?

Memoization is when a complex function stores its output so the next time it is called with the same input. It’s similar to caching, but on a more local level. It can skip any complex computations and return the output faster as it’s already calculated.

This can have a significant effect on memory allocation and performance, and that strain is what the useCallback hook is meant to alleviate.

React’s useCallback vs useMemo

At this point, it’s worth mentioning that useCallback pairs nicely with another hook called useMemo. We’ll discuss them both, but in this piece, we’re going to focus on useCallback as the main topic.

The key difference is that useMemo returns a memoized value, whereas useCallback returns a memoized function. That means that useMemo is used for storing a computed value, while useCallback returns a function that you can call later on.

These hooks will give you back a cached version unless one of their dependencies (e.g. state or props) changes.

Let’s take a look at the two functions in action:

import { useMemo, useCallback } from 'react'
const values = [3, 9, 6, 4, 2, 1]

// This will always return the same value, a sorted array. Once the values array changes then this will recompute.
const memoizedValue = useMemo(() => values.sort(), [values])

// This will give me back a function that can be called later on. It will always return the same result unless the values array is modified.
const memoizedFunction = useCallback(() => values.sort(), [values])

The code snippet above is a contrived example but shows the difference between the two callbacks:

  1. memoizedValue will become the array [1, 2, 3, 4, 6, 9]. As long as the values variable stays, so will memoizedValue, and it’ll never recompute.
  2. memoizedFunction will be a function that will return the array [1, 2, 3, 4, 6, 9].

What’s great about these two callbacks is they become cached and hang around until the dependency array changes. This means that on a render, they won’t get garbage collected.

Rendering and React

Why is memoization important when it comes to React?

It has to do with how React renders your components. React uses a Virtual DOM stored in memory to compare data and decide what to update.

The virtual DOM helps React with performance and keeps your application fast. By default, if any value in your component changes, the entire component will re-render. This makes React “reactive” to user input and allows the screen to update without reloading the page.

You don’t want to render your component because changes won’t affect that component. This is where memoization through useCallback and useMemo comes in handy.

When React re-renders your component, it also recreates the functions you’ve declared inside your component.

Note that when comparing the equality of a function to another function, they will always be false. Because a function is also an object, it will only equal itself:

// these variables contain the exact same function but they are not equal
const hello = () => console.log('Hello Matt')
const hello2 = () => console.log('Hello Matt')

hello === hello2 // false
hello === hello // true

In other words, when React re-renders your component, it will see any functions that are declared in your component as being new functions.

This is fine most of the time, and simple functions are easy to compute and will not impact performance. But the other times when you don’t want the function to be seen as a new function, you can rely on useCallback to help you out.

You might be thinking, “When would I not want a function to be seen as a new function?” Well, there are certain cases when useCallback makes more sense:

  1. You’re passing the function to another component that is also memoized (useMemo)
  2. Your function has an internal state it needs to remember
  3. Your function is a dependency of another hook, like useEffect for example

Performance Benefits of React useCallback

When useCallback is appropriately used, it can help speed up your application and prevent components from re-rendering if they don’t need to.

Let’s say, for example, you have a component that fetches a large amount of data and is responsible for displaying that data in the form of a chart or a graph, like this:

A colorful bar graph comparing the overall transaction time of PHP, MySQL, Reddis, and external (other) in milliseconds.
Bar graph generated using a React component.

Suppose the parent component for your data visualization’s component re-renders, but the changed props or state do not affect that component. In that case, you probably don’t want or need to re-render it and refetch all the data. Avoiding this re-render and refetch can save your user’s bandwidth and provide a smoother user experience.

Drawbacks of React useCallback

Although this hook can help you improve performance, it also comes with its pitfalls. Some things to consider before using useCallback (and useMemo) are:

  • Garbage collection: The other functions that are not already memoized will get thrown away by React to free up memory.
  • Memory allocation: Similar to garbage collection, the more memoized functions you have, the more memory that’ll be required. Plus, each time you use these callbacks, there’s a bunch of code inside React that needs to use even more memory to provide you with the cached output.
  • Code complexity: When you start wrapping functions in these hooks, you immediately increase the complexity of your code. It now requires more understanding of why these hooks are being used and confirmation that they’re used correctly.

Being aware of the above pitfalls can save you the headache of stumbling across them yourself. When considering employing useCallback, be sure the performance benefits will outweigh the drawbacks.

React useCallback Example

Below is a simple setup with a Button component and a Counter component. The Counter has two pieces of state and renders out two Button components, each that will update a separate part of the Counter components state.

The Button component takes in two props: handleClick and name. Each time the Button is rendered, it will log to the console.

import { useCallback, useState } from 'react'

const Button = ({handleClick, name}) => {
  console.log(`${name} rendered`)
  return <button onClick={handleClick}>{name}</button>
}

const Counter = () => {

console.log('counter rendered')
  const [countOne, setCountOne] = useState(0)
  const [countTwo, setCountTwo] = useState(0)
  return (
    <>
      {countOne} {countTwo}
      <Button handleClick={() => setCountOne(countOne + 1)} name="button1" />
      <Button handleClick={() => setCountTwo(countTwo + 1)} name="button1" />
    </>
  )
}

In this example, whenever you click on either button, you’ll see this in the console:

// counter rendered

// button1 rendered
// button2 rendered

Now, if we apply useCallback to our handleClick functions and wrap our Button in React.memo, we can see what useCallback provides us. React.memo is similar to useMemo and allows us to memoize a component.

import { useCallback, useState } from 'react'

const Button = React.memo(({handleClick, name}) => {
  console.log(`${name} rendered`)
  return <button onClick={handleClick}>{name}</button>
})

const Counter = () => {
  console.log('counter rendered')
  const [countOne, setCountOne] = useState(0)
  const [countTwo, setCountTwo] = useState(0)
  const memoizedSetCountOne = useCallback(() => setCountOne(countOne + 1), [countOne)
  const memoizedSetCountTwo = useCallback(() => setCountTwo(countTwo + 1), [countTwo])
  return (
    <>
        {countOne} {countTwo}
        <Button handleClick={memoizedSetCountOne} name="button1" />
        <Button handleClick={memoizedSetCountTwo} name="button1" />
    </>
  )
}

Now when we click either of the buttons, we’ll only see the button we clicked to log into the console:

// counter rendered

// button1 rendered

// counter rendered

// button2 rendered

We’ve applied memoization to our button component, and the prop values that are passed to it are seen as equal. The two handleClick functions are cached and will be seen as the same function by React until the value of an item in the dependency array changes (e.g. countOne, countTwo).

Summary

As cool as useCallback and useMemo are, remember that they have specific use cases — you should not be wrapping every function with these hooks. If the function is computationally complex, a dependency of another hook or a prop passed to a memoized component are good indicators that you might want to reach for useCallback.

We hope this article helped you understand this advanced React functionality and helped you gain more confidence with functional programming along the way!

Matthew Sobieray

Matthew works for Kinsta as a Development Team Lead from his home office in Denver, Colorado. He loves to learn, especially when it comes to web development.