React Hooks - useCallback vs. useMemo

As React functional components grow larger in size and involve more complexity, re-renders become less efficient and may adversely impact the application's performance. The worst the application's performance, the greater the likelihood of users being frustrated with slow and unresponsive pages. If you choose to ignore this declining performance, then users may forever leave your application for alternatives that offer better performance. React provides two built-in hooks for memoizing expensive computations: useCallback and useMemo.

Learning the differences between the two hooks and knowing when to use them ensures that you understand the trade-offs associated with these hooks and don't prematurely optimize your application.

Below, I'm going to show you...

  • The importance of memoization for optimizing the performance of large applications.

  • The differences between useCallback and useMemo.

  • How to use useCallback and useMemo inside of a React functional component.

Understanding Memoization#

With memoization, we avoid re-calculating values by caching previously calculated values. For example, given a simple double function that calculates the product of an input number and two:

Suppose we call the double function five times with the same argument 10...

Each function call performs the exact same multiplication calculation of 10 * 2 and yields the exact same value of 20. Calculating the product between two numbers can be considered a simple operation that requires little time to output a result, so caching would not be suitable in this case. However, for functions with long execution times, such as retrieving a list of suggestions based on user input (typeahead) via an API endpoint, caching could lead to significant performance gains.

For example, if the user visits YouTube looking for videos related to JavaScript and types "JavaScript" into a search typeahead, then the typeahead will display suggestions for the inputted query "JavaScript." If the user decides to narrow down the search to videos related to JavaScript closures and adds "Closures," then the typeahead will display suggestions for the newly inputted query "JavaScript Closures." What if the user changes their mind again and wants videos only related to JavaScript? If the user deletes "Closures" within the typeahead, then the typeahead will once again display suggestions for the inputted query "JavaScript." If we cached the suggestions retrieved from the first time the user entered "JavaScript," then we avoid repeating an API call that would return results previously fetched.

To memoize a function, define a function memoize that...

  1. Initializes a new cache cache for storing previously calculated values. Each input value corresponds to a calculated value. For example, if we memoize the double function, then inside of the cache, an input value of 10 (key) would correspond to a calculated value of 20 (value).

  2. Returns an anonymous closure function that acts as the original function with the same function signature, but also, is capable of memoizing values and returning memoized values. Essentially, the memoize function is a decorator/higher-order function.

Each time a function is memoized, a new cache is created for that particular function. In the above example, instead of calculating the product of 10 and 2 five separate times, the product is calculated only once, on the first function call. The benefits of memoization are more apparent for functions that require more time to finish execution.

Memoization comes with the trade-off of saving execution time in exchange for greater memory consumption. Also, the above implementation of the memoize function works best with a pure function, which always evaluates the same result when given the same argument/s (deterministic) and causes zero side-effects. For an impure function, whether you should memoize it or not depends on its implementation. For example, with the typeahead example, because suggestions don't change frequently and users don't necessarily need the most recently updated suggestions with few to zero suggestions changed, the function for fetching these suggestions can be memoized. There are additional limitations to the above implementation of the memoize function, such as eventually running out of memory (and needing to implement a cache invalidation strategy) and memoizing based on multiple arguments rather than a single argument, but those topics are outside of the scope of this tutorial.

The useCallback Hook#

To understand why the useCallback hook is important, let's walkthrough a simple example with a component that renders a list of numbers. Anytime an "Add Item" button is clicked, a new number is added to the list.

(src/App.jsx)

(src/components/List.jsx)

Initially, the list starts with only one number, zero.

If you press the "Add Item" button, then the number one is added to the end of the list. The number added to the list by the "Add Item" button is one more than the last number in the list.

If you press the "Add Item" button again, then the number two is added to the end of the list.

Each time a number is added to the list, the numbers state variable is updated with a newly concatenated number, and React re-renders the components affected by this update along with those components' children: the <App /> component (where the state change occurred) and the <List /> component (where a prop changed as a result of the state change).

Anytime it re-renders a component, React recreates the functions defined within the body of the functional component as brand-new function objects.

To track whether the component recreates the functions defined within it during subsequent re-renders:

  1. Define a set outside of the functional component.

  2. Within the body of the functional component, add the function to this set and log the contents of the set. If the set grows in size on subsequent re-renders, then the functional component is recreating its function/s.

(src/components/List.jsx)

If we restart the example application and open the developer tools, then you will notice only one function is within the set.

If you press the "Add Item" button, then another function is added to the set.

This means the <List /> component recreates the handleOnClick function anytime React re-renders it even though nothing about handleOnClick has changed.

If we wrap the handleOnClick function with the useCallback hook, then the <List /> component only creates one instance of this function and memoizes it. Upon subsequent re-renders, the <List /> component does not recreate this function since it is already memoized.

(src/components/List.jsx)

Now, if you press the "Add Item" button multiple times, only one function will be stored within the set.

The useCallback hook accepts two arguments:

  • A function (called an inline callback) to memoize.

  • A dependencies array. Anytime a dependency changes, the function is recreated, and this brand-new function object is memoized. React uses referential equality to decide if a dependency has changed. For example, if a prop is added to a dependency array, then anytime this prop changes within the parent component, React decides that the component has changed since the previous prop is not the same as the new prop reference-wise. Remember, {} === {} evaluates to false because both object literals exist in different memory locations despite both being empty object literals.

Currently, in the above example, the dependency array is empty. Therefore, this function remains the same across all subsequent re-renders.

If we add items as a dependency, then each time we press the "Add Item" button, a new function is stored within the set. For each re-render, useCallback returns a different function instance.

(src/components/List.jsx)

In practice, dependencies should be specified only if the function references the dependency during execution. Suppose we adjust the <List /> component's handleOnClick function to print the value of a prop randomNumber instead of evt.target.innerHTML, like so:

(src/components/List.jsx)

Because this function relies on the value of this prop during execution, it should be recreated whenever this prop changes. Therefore, randomNumber is added to the dependency array. This component receives this randomNumber prop from the parent <App /> component. Within this component, let's add another button to set a randomNumber state variable when clicked. randomNumber is set to a random number generated by Math.random.

(src/App.jsx)