Andrew Evans Climbing mountains and tackling technology challenges for over eight years in the mile high state.

React useMemo vs. useCallback: A pragmatic guide

8 min read 2309

React useMemo vs. useCallback: A pragmatic guide

Editor’s note: This guide to React useMemo vs. useCallback was last updated by Shalitha Suranga on 18 May 2023 to reflect recent changes to React and address how to improve performance in your React apps with useMemo and useCallback.

As HTML pages grow in size and complexity, creating efficient React code becomes more important than ever. Re-rendering large components is costly, and providing a significant amount of work for the browser through a single-page application (SPA) increases processing time and can potentially drive away users. React offers inbuilt API features to improve app performance by avoiding unnecessary re-renders, caching repetitive costly operations, lazy-loading components, etc.

This tutorial examines two different React Hooks, useMemo and useCallback. These Hooks help developers improve the rendering performance of components, preserve objects between React component renderings, and help improve application performance.

Jump ahead:

React performance optimizations for functions

React already provides React.memo() to avoid recreating DOM elements when props are not changed. This method is a higher-order component (HOC) that memoizes the last result. But, it doesn’t memoize typical JavaScript functions. Therefore, despite being a first-class citizen in JavaScript, functions may potentially be recreated with every use.

The useMemo and useCallback methods help to avoid recreating or rerunning functions in certain situations. Although not always useful, useMemo or useCallback may create a noticeable difference when dealing with large amounts of data or many components that share behavior. For example, this would be especially useful when you’re creating a stock or digital currency trading platform.

What is useCallback?

When React re-renders a component, function references inside the component get re-created. If you pass a callback function to a memoized (with React.memo) child component via props, it may get re-rendered even if the parent component doesn’t apparently change the child component’s props. Each parent component re-rendering phase creates new function references for callbacks, so inequal callback props can trigger an unwanted child component re-render silently even visible props don’t get changed.

The useCallback React Hook returns a memoized function reference based on a function and dependencies array. So, we can use it to create optimized callbacks that don’t cause unwanted re-renders. This Hook returns a cached (memoized) function reference if dependencies aren’t changed.

A practical callback example for useCallback

Now, let’s get started. First, create a React project on your computer with Create React App to follow along:

npx create-react-app PerformanceHooks

Or, you can see or edit the upcoming examples in provided CodeSandbox links. Now, look at the following example source code that passes a callback function to a memoized child component:

 import { memo, useState } from "react";
import "./styles.css";

const Numbers = memo(({ nums, addRandom }) => {
  console.log("Numbers rendered");

  return (
    <div>
      <ul>
        {nums.map((num, i) => (
          <li key={i}>{num}</li>
        ))}
      </ul>
      <button onClick={addRandom}>Add random</button>
    </div>
  );
});

export default function App() {
  const [nums, setNums] = useState([]);
  const [count, setCount] = useState(0);

  const increaseCounter = () => {
    setCount(count + 1);
  };

  const addRandom = () => {
    let randNum = parseInt(Math.random() * 1000, 10);
    setNums([...nums, randNum]);
  };

  return (
    <div>
      <div>
        Count: {count} &nbsp;
        <button onClick={increaseCounter}>+</button>
      </div>
      <hr />
      <Numbers nums={nums} addRandom={addRandom} />
    </div>
  );
}

The above source code renders a counter in the App component and a random number list in the Numbers component. React will re-render the Numbers component when the counter increases, even if it’s optimized with memo, as shown below:

Clicking the Counter Increment Button Causes a Re-Render of the Numbers Component

The reason is that whenever the App component re-renders, it re-creates a function reference for the addRandom callback. The Numbers component gets re-rendered since the props are different! As a solution, we can wrap addRandom with useCallback, as shown in the following code snippet:

  const addRandom = useCallback(() => {
    let randNum = parseInt(Math.random() * 1000, 10);
    setNums([...nums, randNum]);
  }, [nums]);

The above solution eliminates the previously discussed unwanted re-render since addRandom receives a cached function reference.

Pros and cons of using useCallback

See the following table to understand the pros and cons of using useCallback as a performance enhancement in your React apps:

useCallback pros useCallback cons
Helps developers cache a function to avoid excessive re-renders of a child component Adds excessive syntax for callback definition, so use of useCallback may complicate your code
Developers can improve the use of memo built-in’s performance enhancements (See the previous example) Cannot be used to efficiently and properly cache a value as useMemo
Comes as an inbuilt, stable React core feature that we can use in production The usage of this Hook may confuse React newcomers since it caches a function — not a simple value
Offers an easy function interface that accepts just two parameters: a function and dependencies array Excessive usage can lead to memory-related performance issues

What is useMemo?

In some scenarios, we have to include complex calculations in our React components. These complex calculations are inevitable and may slow down the rendering flow a bit. If you had to re-render a component that handles a costly calculation to update another view result (not the result of the costly calculation), the costly calculation may get triggered again, ultimately causing performance issues. This situation can be solved by caching the complex calculation result.

The useMemo Hook serves a similar purpose as useCallback, but it returns a memoized value instead of a function reference. This allows you to avoid repeatedly performing potentially costly operations until necessary. The useMemo Hook typically returns a cached value until a dependency gets changed. If a dependency gets changed, React will re-do the expensive calculation and updates the memoized value.

A practical example for useMemo

Look at the following source code that does a costly calculation with one state field:

import { useState } from "react";
import "./styles.css";
export default function App() {
  const [nums, setNums] = useState([]);
  const [count, setCount] = useState(1);
  const increaseCounter = () => {
    setCount(count + 1);
  };
  const addRandom = () => {
    let randNum = parseInt(Math.random() * 1000, 10);
    setNums([...nums, randNum]);
  };
  const magicNum = calculateMagicNumber(count);
  return (
    <div>
      <div>
        Counter: {count} | Magic number: {magicNum} &nbsp;
        <button onClick={increaseCounter}>+</button>
      </div>
      <hr />
      <div>
        <ul>
          {nums.map((num, i) => (
            <li key={i}>{num}</li>
          ))}
        </ul>
        <button onClick={addRandom}>Add random</button>
      </div>
    </div>
  );
}
function calculateMagicNumber(n) {
  console.log("Costly calculation triggered.");
  let num = 1;
  for (let i = 0; i < n + 1000000000; i++) {
    num += 123000;
  }
  return parseInt(num - num * 0.22, 10);
}

The above code implements two main functional segments in the app:

  • Counter value, magic number, and counter increment button
  • Random numbers list with a button to add more random numbers

Once you run the app, it will work as expected. It will calculate a magic number for the current counter value and will add new random numbers to the list when you click the Add random button. Magic number calculation is costly, so you’ll feel a delay once you increase the counter value.



There is a hidden issue. Why does the Add random button work so slowly as the magic number generation process? Look at the following preview:

Example of Clicking the “Add Random” Button

The reason is that the Add random button also triggers a mandatory re-render which triggers the calculateMagicNumber slow function. As a solution, we can wrap the calculateMagicNumber function call with useMemo to let React use a cache value when the App component re-renders via the Add random button:

const magicNum = useMemo(() => calculateMagicNumber(count), [count]);

Now, the useMemo Hook calculates a new magic number only if the count dependency gets changed, so the Add random will work faster! You can check out the optimized example here:

Pros and cons of using useMemo

See the following table to understand the pros and cons of using useMemo as a performance enhancement in your React apps:

useMemo pros useMemo cons
Helps developers cache a value to avoid unwanted costly recalculations Adds excessive syntax for compute function calls, so use of useMemo may complicate your code
Able to use with inbuilt memo when a child component uses a computed object that doesn’t need frequent re-computations Can be used to cache a function, but it affects readability (Use useCallback for caching functions)
Comes as an inbuilt, stable React core feature that we can use in production React newcomers may use this Hook for situations where caching isn’t needed, such as with simple calculations, frequently changed values, etc. As a result, code readability and app memory usage will get affected
Offers an easy function interface that accepts just two parameters: a function and dependencies array Excessive usage can lead to memory-related performance issues

Referential equality in useMemo and useCallback

A React library often needs to check the equality of two identifiers. For example, when you update a component state field, React needs to check whether the previous state field is not equal to the current one before triggering a new re-render. Similarly, useMemo and useCallback needs to check the equality of identifiers for invaliding the cached items.

In JavaScript, equality checking for primitives is straightforward because they are immutable (cannot be changed without creating a new one). But, objects and functions are mutable. If React used deep comparison for objects and functions, there will be performance drawbacks. So, React uses JavaScript’s referential equality concept for comparing two identifiers.

Referential equality refers to comparing identifiers based on their references. For example, the following code snippet prints true two times since object and function references are equal:

let a = {msg: 'Hello'};
let b = a;
b.msg = 'World';
console.log(a === b); // true

let c = () => {};
let d = c;
console.log(c === d); // true

The following code snippet prints false two times even if identifier data look the same:

let a = {msg: 'Hello'};
let b = {msg: 'Hello'};
console.log(a === b); // false

let c = () => 100;
let d = () => 100;
console.log(c === d); // false

The above code snippet prints false two times since identifier references are different. React internally uses this referential equality check via the Object.is() method (works the same as === as we tested before) to detect changes between states.

When we don’t use useCallback, React triggered an unwanted re-render since referential equality was false as the callback gets re-created in every re-render. Similarly, if we create a complex object in a component via an expensive function without useMemo, it will slow down all re-renders since referential equality becomes false. Both useMemo and useCallback let you set referential quality to true by returning cached unchanged references.

Working with useCallback vs. useMemo in React

The useCallback and useMemo Hooks appear similar on the surface. However, there are particular use cases for each.

Wrap functions with useCallback when:

  • React.memo()-wrapped component accepts a callback function as a prop
  • Passing a callback function as a dependency to other Hooks (i.e., useEffect)

Use useMemo when:

  • For expensive calculations that should be cached when unrelated re-renders happen
  • Memoizing a dependency of another Hook

A callback works well when code would otherwise be recompiled with every call. Memoizing results can help decrease the cost of repeatedly calling functions when the inputs change gradually over time. On the other hand, in the trading example, we may not want to memoize results for a constantly changing order book.

How to improve your React performance with useMemo and useCallback

If your React app triggers excessive, unwanted re-renders and has slow processing before each re-render, it may use more CPU and memory. This situation won’t be noticeable for users through small apps. But, large apps that have critical rendering performance issues can slow down users’ computers, reducing usability factors and product quality. React helps you solve critical rendering-related performance issues with useMemo and useCallback.

Here is a checklist that you can follow to improve React app performance with useMemo and useCallback:

  • First, you need to accept that there is a performance issue. See it through your code (by doing a personal code review or peer review) or feel it from your app
  • Measure actual rendering performance and code execution time using Chrome DevTools, console.time(), or React Profiler. Detect unwanted re-renders
  • Decide and use useMemo or useCallback
  • Use these performance-related Hooks and measure performance again

Overusing useMemo and useCallback may worsen existing performance issues, so let’s explain the anti-patterns below.

useCallback and useMemo anti-patterns

It can be tempting to think you can use useCallback or useMemo for every function. However, this is not the case. There is overhead associated with wrapping functions. Each call requires extra work to unravel your function call and decide how to proceed.

Notice how the increaseCounter function is not a callback that we need to add useCallback while the addRandom function is. The increaseCounter function does not meet our criteria, as it is created once and never shared with child components.

In contrast, each changing list item in the Number component uses the addRandom function. Similarly, we wrapped the calculateMagicNumber function call using useMemo, but would never wrap functions that deal with frequently changing data with useMemo.

Conclusion

The useCallback and useMemo functions are instruments for fine-tuning React. Knowing how and when to use each could potentially improve application performance. Still, no inbuilt performance-improvement Hook is a substitute for a poorly written React app codebase. Here, we’ve provided a guide for understanding how to use these tools, but keep in mind that using them comes with a cost (memory usage for caching).

Get set up with LogRocket's modern React error tracking in minutes:

  1. Visit https://logrocket.com/signup/ to get an app ID.
  2. Install LogRocket via NPM or script tag. LogRocket.init() must be called client-side, not server-side.
  3. $ npm i --save logrocket 

    // Code:

    import LogRocket from 'logrocket';
    LogRocket.init('app/id');
    Add to your HTML:

    <script src="https://cdn.lr-ingest.com/LogRocket.min.js"></script>
    <script>window.LogRocket && window.LogRocket.init('app/id');</script>
  4. (Optional) Install plugins for deeper integrations with your stack:
    • Redux middleware
    • ngrx middleware
    • Vuex plugin
Get started now
Andrew Evans Climbing mountains and tackling technology challenges for over eight years in the mile high state.

One Reply to “React useMemo vs. useCallback: A pragmatic guide”

  1. Thanks for the article. You mentioned trader for this example. I wonder how much time it could save here, maybe less than 1ms, my guts feeling, which means this has to be very high frequency trader 🙂

Leave a Reply