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)

Restart the application. If we click the "Add Item" button, then the number of handleOnClick functions stored within the set functionStore remains one regardless of how many times we click this button.

However, if we click the "Generate Number" button, then the randomNumber state variable within the <App /> component is updated to a new value and causes a re-render, which causes the <List /> component's handleOnClick function to be recreated since the randomNumber prop, a dependency, has changed. Now the set has two functions stored within it.

Try out the above useCallback hook example in the CodeSandbox below:

Note: If you see two functions being added to the set during any single re-render, then disable React Strict Mode from the root ReactDOM.render method since it runs certain callbacks/methods twice in the development environment.

The useMemo Hook#

Let's quickly recap the purpose of the useCallback hook. This hook accepts an inline callback and a dependencies array. It memoizes the callback and returns the memoized callback. Whenever a dependency changes, React recreates the callback, memoizes this newly created callback with useCallback, which returns the newly memoized function.

The useMemo hook accepts a "create" function and a dependencies array. The "create" function must return a value. useMemo memoizes the value returned by this function and returns the memoized value. Whenever a dependency changes, useMemo executes the "create" function to recompute the value, memoizes this value and returns the newly memoized value. If a dependencies array is not provided, then the value is recomputed on each re-render.

This hook proves valuable for avoiding unnecessary expensive calculations involving large datasets. Suppose a component renders a large list of items and receives an items prop that is an array of items responsible for rendering the list of items. If the items of this items prop need to be normalized or transformed prior to rendering the list of items, then rendering the large list of items will be slow due to having to perform this extra step.

Let's take our previous useCallback example and make several adjustments to the <List /> component to illustrate this scenario.

(src/components/List.jsx)

Here, the <List /> component transforms the items prop by converting each item to an object that preserves the original item's value and contains an additional field that holds a random number generated via Math.random. Then, it renders these new items.

If we click the "Generate Number" button, then notice that the value of the random field changes:

Clicking the "Generate Number" button changes the value of the randomNumber prop, which causes the <List /> component to recompute transformedItems despite this computation not involving this prop whatsoever.

To avoid recomputing transformedItems whenever the randomNumber prop changes and only recomputing it when the items prop changes, memoize it with the useMemo hook:

(src/components/List.jsx)

Now, whenever we click the "Generate Number" button, the value of the random field remains unchanged. If we click the "Add Item" button, then the <List /> component receives the updated items prop and recomputes transformedItems since the items dependency has changed.

Try out the above useMemo hook example in the CodeSandbox below:

Next Steps#

Try using the useMemo and useCallback hooks in your React applications. Remember to profile your application to identify its actual performance bottlenecks before using these hooks!

Sources#