Using useCallback Hook in React — A Developer's Guide

Dillion Megida
June 7, 2022

TABLE OF CONTENTS

React has so many in-built hooks that allow functional components to plug into the lifecycle of the component and perform different operations. There’s the useState hook, useEffect hook, useLayoutEffect hook and many more. In this article, we’ll be looking at the useCallback hook. We’ll learn about the function of this hook, the relevance, a common use case for it, and we’ll also compare it with the useMemo hook.

What is the useCallback hook in React?

The useCallback hook returns a memoized callback function. Memoization is an optimization technique in program development that stores some computed value in memory and can be accessed without having to recompute that value.

Think of memoization as a cache, such that as long as the inputs don’t change, the cached value is used. But if the inputs change, the value is recomputed.

This callback function from the useCallback hook only changes when the specified dependencies that the function depends on, changes. Here’s the syntax:


const func = useCallback((args) => {
  // some code here
}, [dependencies])

The dependencies array specifies what this hook depends on. When any of the dependency changes, the hook redeclares the function and caches it.

Why do we need the useCallback hook?

In general, memoization is a great way to improve program speed, and improve user experience by avoiding to recompute the same values over and over again. The useCallback hook helps us to achieve this in functional components.

With this hook, we can save expensive functions (slow functions, or functions using many resources) in memory, and only have to execute them when we need to.

Example/Usecase of the useCallback hook

Avoiding recomputations due to referential equality

The most common reason why you would want to use this hook is to avoid referential equality problems. And this is found in objects. In JavaScript, objects, no matter how similar are not equal to each other because they have different references.

This inequality in references is very common when you’re using a function as a dependency, for example, in a useEffect hook. This may cause the useEffect hook to be triggered when you do not expect. Let’s look at some React code to understand this better.

Let’s say we have the following project.


// src/components/product-list.js

import React, { useState, useEffect } from 'react'

export default function ProductList({ getProducts }) => {
  const [products, setProducts] = useState([])

  useEffect(() => {
    setProducts(getProducts())    
  }, [getProducts])

  return (...) // display the products on the UI
}

// src/App.js

import React from 'react'
import ProductList from './components/product-list

export default function App() {
  const [query, setQuery] = useState("")
  const [number, setNumber] = useState(0)

  const getProducts = () => {
    // some API call here, which uses the query state
  }

  return (
    <div>
      {/* <input /> component here which updates the query state */}
      <ProductList getProducts={getProducts} />
    </div>
  ) 
}

This basic project has two components: the ProductList component which takes a getProduct function prop and the App component.

The getProduct function is passed from the App component to the ProductList component, which calls the function in a useEffect hook, updates the products state, and displays the returned value on the UI. The getProduct function is also a dependency for the useEffect hook so that the function is only called when it changes.

This getProduct function is assumed to make an API call (which could be slow) when it is evoked. Here’s the problem with this setup.

In React, when the state in a component changes, the component is re-rendered, every value is recomputed and every declaration re-declared. This means that when the query state changes:

  • the getProducts function is redeclared, and the getProducts function in the previous state is NOT EQUAL to the getProducts function in the new state (because in JavaScript. objects no matter how similar are not equal to each other)
  • in the ProductList component, the getProducts function is re-executed because the useEffect hook believes that the getProducts dependency has changed

This is what we want. The query changes, and a new set of products is fetched and displayed on the screen. But, what if the number state changes? Same thing happens. The get products function is redeclared,  the get products function re-executed again in the Product List component.

As a way to improve this, we want to optimize this component to “save” the get products function and not to have to make API calls every time the state changes. The only time we want to make a new API call is if the query state changes, so that we can get a new set of products.

We can improve this with the useCallback hook like so:


// src/App.js

import React, { useCallback } from 'react'
import ProductList from './components/product-list

export default function App() {
  const [query, setQuery] = useState("")
  const [number, setNumber] = useState(0)

  const getProducts = useCallback(() => {
    // some API call here, which uses the query state
  }, [query])

  return (
    <div>
      {/* <input /> component here which updates the query state */}
      <ProductList getProducts={getProducts} />
    </div>
  ) 
}

By memoizing the getProducts function, it does not get redeclared on rerender when the number state changes. This means, the reference stays equal in the useEffect hook of the List component, hence, the useEffect hook is not triggered.

By passing a query dependency, the getProducts function will be redeclared when the query state changes, and also triggering the useEffect hook—which is just what we want.

When should you not use the useCallback hook

You shouldn’t use the useCallback for solving every reference equality problems. Yes, it seems like a nice solution to avoid redeclarations all the time but one thing to note here is, memoization is a technique that involve saving some data to memory. Memoizing every part of your application uses more memory resources and this can negatively affect the performance of your application.

You should only use this hook when a function repeatedly executed can result in a bad user experience. For example, expensive or slow API calls. But for a function that does simple calculations, there’s no problem having that executed every time.

useCallback vs useMemo

Just like the useCallback hook, the useMemo hook is used for memoizing values in functional components. The difference between both hooks is that useCallback returns a memoized callback function while useMemo returns a memoized value.

That is, on using the useMemo hook, the callback function passed to it is executed, and a memoized value returned to the variable. But with the useCallback hook, the callback function passed to it is memoized and returned to the variable.

Here’s a code block to explain this:


const var1 = useMemo(() => {
  return 1 + 1
}, [...])

const var2 = useCallback(() => {
  return 1 + 1
}, [...])

console.log(var1)
// 2
console.log(var2)
// () => { return 1 + 1 }

It’s also worth noting that the useCallback takes a callback function as an argument which can also have arguments. For example:


const var1 = useMemo((number) => {
  return 1 + number
}, [...])

// ...
var1(50)

This makes it easy to reuse functions. But you cannot do the same with the useMemo hook.

Conclusion

In this article, we’ve seen what the useCallback hook is, how it works, a use case for it, when you shouldn’t use it and how it compares to the useMemo hook.

To reiterate, this hook is used for memoizing functions in React, which can be very useful for executing such functions when you need to, and not when a random state changes.