We fully understand useMemo and useCallback

A tour of two of the most famous hooks in React

If you've been struggling to understand useMemo and useCallback, you're not alone! I've talked to many React developers who are scratching their heads over these two hooks.

My goal here is to clear up all this confusion. We'll learn what they do, why they're useful, and how to get the most out of them.

Let's go!

The target audience

This guide is written to help beginner/mid-level developers get comfortable with React. If you're taking your first steps in React, bookmark this step and come back to it in a few weeks!

main idea

Okay, let's start with useMemo.

main idea useMemo is that it allows us “remember” calculated value between renders.

This definition requires some decoding. In fact, this requires a fairly complex mental model of how React works – let's get that out of the way first.

The main thing that React does is synchronize our interface with the state of our application. The tool he uses for this is called “re-rendering

Each re-render is a snapshot of what the application's interface should look like at a given point in time, based on the current state. We can think of it as a stack of photographs, each representing what everything should look like given all the values ​​for each state variable.

Each “re-render” creates a mental picture of what the DOM should look like based on its current state. The picture above shows it as HTML, but it's actually a bunch of JS objects. You've probably heard this term; it's sometimes called “virtual DOM

We don't directly tell React which DOM nodes need to change. Instead, we tell React what must be an interface based on the current state. When React re-renders, it creates a new snapshot and can determine what needs to be changed by comparing snapshots, like a game of spot-10-differences.

React is heavily optimized out of the box, so re-rendering isn't a big deal in general. But in some situations, creating such images takes some time. This may lead to performance problemsfor example, the interface does not update quickly enough after the user performs an action.

Roughly speaking, useMemo And useCallback are tools designed to help us optimize re-rendering. They do this in two ways:

  1. Reducing the amount of work that needs to be done on each render.

  2. Reducing the number of times a component needs to be redrawn at all.

Let's discuss these cases separately.

Use case 1: Heavy calculations

Let's say we create a tool that helps users find all prime numbers from 0 to selectedNumWhere selectedNum is the value entered by the user. A prime number is a number that can only be divided by 1 and itself, such as 17.

Here is one possible implementation:

Example code
import React from 'react';

function App() {
  // Мы сохраняем выбранный пользователем номер в состоянии.
  const [selectedNum, setSelectedNum] = React.useState(100);
  
  // Мы вычисляем все простые числа от 0 до выбранного пользователем 
  // числа «selectedNum»:
  const allPrimes = [];
  for (let counter = 2; counter < selectedNum; counter++) {
    if (isPrime(counter)) {
      allPrimes.push(counter);
    }
  }
  
  return (
    <>
      <form>
        <label htmlFor="num">Your number:</label>
        <input
          type="number"
          value={selectedNum}
          onChange={(event) => {
            // Чтобы компьютеры не взрывались, мы ограничимся 100 тысячами.
            let num = Math.min(100_000, Number(event.target.value));
            
            setSelectedNum(num);
          }}
        />
      </form>
      <p>
        There are {allPrimes.length} prime(s) between 1 and {selectedNum}:
        {' '}
        <span className="prime-list">
          {allPrimes.join(', ')}
        </span>
      </p>
    </>
  );
}

// Вспомогательная функция, которая определяет, 
// является ли данное число простым или нет.
function isPrime(n){
  const max = Math.ceil(Math.sqrt(n));
  
  if (n === 2) {
    return true;
  }
  
  for (let counter = 2; counter <= max; counter++) {
    if (n % counter === 0) {
      return false;
    }
  }

  return true;
}

export default App;

I don't expect you to read every line of code here, so here are the highlights:

  • We have one state, a number called selectedNum.

  • Using a loop forwe manually calculate all prime numbers from 0 to selectedNum.

  • We visualize the number input so the user can change selectedNum.

  • We show the user all the prime numbers that we have calculated.

This code requires significant amount of calculations. If the user selects a large value selectedNum, we'll have to go through tens of thousands of numbers, checking to see if each one is prime. While there are more efficient prime number checking algorithms than the one I used above, they will always be computationally intensive.

Sometimes we actually need to do this calculation, for example when the user selects a new one selectedNum. But we could potentially run into some performance issues if we do this work free of chargewhen it is not necessary.

For example, let's say our example also has a digital clock:

import format from 'date-fns/format';

function App() {
  //тот же код
  
  // `time` — это переменная состояния, которая меняется 
  // раз в секунду, поэтому она всегда синхронизируется с текущим временем.
  const time = useTime();
  
  //тот же код
  
  return (
    <>
      <p className="clock">
        {format(time, 'hh:mm:ss a')}
      </p>
      //...
    </>
  );
}

function useTime() {
  const [time, setTime] = React.useState(new Date());
  
  React.useEffect(() => {
    const intervalId = window.setInterval(() => {
      setTime(new Date());
    }, 1000);
  
    return () => {
      window.clearInterval(intervalId);
    }
  }, []);
  
  return time;
}

//тот же код

export default App;

Our application now has two states: selectedNum And time. Once per second, the time variable is updated to reflect the current time, and this value is used to display the clock in the upper right corner.

That's the problem: Whenever any of the state variables change, we re-run all those expensive prime number calculations. And since time changes once per second, this means that we constantly re-generating this list of prime numbers, even if the user's chosen number has not changed!

In JavaScript, we only have one main thread, and we keep it busy by running this code over and over again, every second. This means that the app may be slow when the user is trying to do other things, especially on lower-end devices.

But what if we could “skip” these calculations? If we already have a list of primes for a given number, why not reuse that value instead of calculating it from scratch each time?

This is what allows us to do useMemo. This is what it looks like:

const allPrimes = React.useMemo(() => {
  const result = [];
  for (let counter = 2; counter < selectedNum; counter++) {
    if (isPrime(counter)) {
      result.push(counter);
    }
  }
  return result;
}, [selectedNum]);

useMemo takes two arguments:

  1. A piece of code that needs to be executed, wrapped in a function

  2. List of dependencies

At mount time, when this component is rendered for the first time, React calls this function to run all the logic, calculating prime numbers. Everything we return from this function is assigned to a variable allPrimes.

Now for each subsequent rendering in React there is a choice:

  1. Call the function again to recalculate the value, or

  2. Reuse data since the last execution.

To answer this question, React looks at the provided list of dependencies. Has any of them changed since the previous render? If so, React will rerun the function to calculate the new value. Otherwise, all this work will be skipped and the previously calculated value will be reused.

useMemo is essentially like a small cache, and dependencies are a strategy for modifying that cache.

In this case, we are essentially saying: “recalculate list of prime numbers only when changed selectedNum” When a component is re-rendered for other reasons (for example due to a time state change), useMemo ignores the function and passes the cached value.

This is commonly known as memoizationand that's why this hook is called “useMemo

Here is a working version of this solution:

Full version of the code
import React from 'react';
import format from 'date-fns/format';

function App() {
  const [selectedNum, setSelectedNum] = React.useState(100);
  const time = useTime();
  
  const allPrimes = React.useMemo(() => {
    const result = [];
    
    for (let counter = 2; counter < selectedNum; counter++) {
      if (isPrime(counter)) {
        result.push(counter);
      }
    }
    
    return result;
  }, [selectedNum]);
  
  return (
    <>
      <p className="clock">
        {format(time, 'hh:mm:ss a')}
      </p>
      <form>
        <label htmlFor="num">Your number:</label>
        <input
          type="number"
          value={selectedNum}
          onChange={(event) => {
            // To prevent computers from exploding,
            // we'll max out at 100k
            let num = Math.min(100_000, Number(event.target.value));
            
            setSelectedNum(num);
          }}
        />
      </form>
      <p>
        There are {allPrimes.length} prime(s) between 1 and {selectedNum}:
        {' '}
        <span className="prime-list">
          {allPrimes.join(', ')}
        </span>
      </p>
    </>
  );
}

function useTime() {
  const [time, setTime] = React.useState(new Date());
  
  React.useEffect(() => {
    const intervalId = window.setInterval(() => {
      setTime(new Date());
    }, 1000);
  
    return () => {
      window.clearInterval(intervalId);
    }
  }, []);
  
  return time;
}

function isPrime(n){
  const max = Math.ceil(Math.sqrt(n));
  
  if (n === 2) {
    return true;
  }
  
  for (let counter = 2; counter <= max; counter++) {
    if (n % counter === 0) {
      return false;
    }
  }

  return true;
}

export default App;

Alternative Approach

So, hook useMemo may help us avoid unnecessary calculations… but really is this the best solution?

Often we can avoid the need to use useMemo by restructuring our application.

Here's one way:

import React from 'react';

import Clock from './Clock';
import PrimeCalculator from './PrimeCalculator';

function App() {
  return (
    <>
      <Clock />
      <PrimeCalculator />
    </>
  );
}

export default App;

I extracted two new components: Clock And PrimeCalculator. Separated from App, each of these two components manages its own state. Re-rendering one component will not affect the other.

We hear a lot about raising state, but sometimes the best approach is to pass our state down! Each component should have one responsibility, and in the example above, the application was doing two completely unrelated things.

This will not always be possible. In a large real-world application, there are many states that need to be raised quite high and that cannot be lowered.

I have one more trick for this case.

Let's look at an example. Let's say we need the time variable to be raised higher PrimeCalculator:

import React from 'react';
import { getHours } from 'date-fns';

import Clock from './Clock';
import PrimeCalculator from './PrimeCalculator';

// Превращаем наш PrimeCalculator в чистый компонент:
const PurePrimeCalculator = React.memo(PrimeCalculator);

function App() {
  const time = useTime();

  // Придумываем подходящий цвет фона, исходя из времени суток:
  const backgroundColor = getBackgroundColorFromTime(time);

  return (
    <div style={{ backgroundColor }}>
      <Clock time={time} />
      <PurePrimeCalculator />
    </div>
  );
}

const getBackgroundColorFromTime = (time) => {
  const hours = getHours(time);
  
  if (hours < 12) {
    // A light yellow for mornings
    return 'hsl(50deg 100% 90%)';
  } else if (hours < 18) {
    // Dull blue in the afternoon
    return 'hsl(220deg 60% 92%)'
  } else {
    // Deeper blue at night
    return 'hsl(220deg 100% 80%)';
  }
}

function useTime() {
  const [time, setTime] = React.useState(new Date());
  
  React.useEffect(() => {
    const intervalId = window.setInterval(() => {
      setTime(new Date());
    }, 1000);
  
    return () => {
      window.clearInterval(intervalId);
    }
  }, []);
  
  return time;
}

export default App;

Like a force field React.memo surrounds our component and protects it from extraneous updates. Our PurePrimeCalculator will be redrawn only when new data is received or when its internal state changes.

This is the so-called pure component. Essentially, we're telling React that this component will always produce the same output given the same input, and we can skip re-renders where nothing has changed.

More traditional approach

In the example above I am using React.memo to the imported component PrimeCalculator.

To be honest, it's a little unusual. I decided to structure it so that everything would be visible in one file and would be easier to understand.

In practice I often use React.memo For export components, for example:

// PrimeCalculator.js
function PrimeCalculator() {
  /* Наполнение компонента */
}
export default React.memo(PrimeCalculator);

Our component PrimeCalculator now it will always be”clean”and we won't have to mess with it when we get ready to use it.

If we ever need”unclean” version PrimeCalculator, we will be able to export the component as a named export. Although I don't think I've ever had to do that.

There's an interesting shift in perspective here.: we used to remember the result of a certain calculation, but in this case I remembered all component.

In any case, expensive calculations will be repeated only when the user selects a new one selectedNum. But we optimized the parent component, not individual slow lines of code.

I'm not saying that one approach is better than another – each tool has its place. But in this particular case, I prefer this approach.

Now, if you've ever tried to use pure components in the real world, you've probably noticed one thing: pure components are often redrawneven if it seems like nothing has changed!

This brings us smoothly to the second problem that it solves useMemo.

Use case 2: Saved links

In the example below I have created a component Boxes. It displays a set of bright boxes that can be used for some decorative purposes.

And also the unlinked state is the username.

Code

App.js

import React from 'react';

import Boxes from './Boxes';

function App() {
  const [name, setName] = React.useState('');
  const [boxWidth, setBoxWidth] = React.useState(1);
  
  const id = React.useId();
  
  // Попробуйте поменять какие-нибудь из этих значений!
  const boxes = [
    { flex: boxWidth, background: 'hsl(345deg 100% 50%)' },
    { flex: 3, background: 'hsl(260deg 100% 40%)' },
    { flex: 1, background: 'hsl(50deg 100% 60%)' },
  ];
  
  return (
    <>
      <Boxes boxes={boxes} />
      
      <section>
        <label htmlFor={`${id}-name`}>
          Name:
        </label>
        <input
          id={`${id}-name`}
          type="text"
          value={name}
          onChange={(event) => {
            setName(event.target.value);
          }}
        />
        <label htmlFor={`${id}-box-width`}>
          First box width:
        </label>
        <input
          id={`${id}-box-width`}
          type="range"
          min={1}
          max={5}
          step={0.01}
          value={boxWidth}
          onChange={(event) => {
            setBoxWidth(Number(event.target.value));
          }}
        />
      </section>
    </>
  );
}

export default App;

Boxes.js

import React from 'react';

function Boxes({ boxes }) {
  return (
    <div className="boxes-wrapper">
      {boxes.map((boxStyles, index) => (
        <div
          key={index}
          className="box"
          style={boxStyles}
        />
      ))}
    </div>
  );
}

export default React.memo(Boxes);

Boxes is a pure component due to the use React.memo() around default export to Boxes.js. This means that it should only re-render when the props change.

And yetwhenever the user changes his name, the Boxes are also redrawn!

What the hell?! Why a force field React.memo() not protecting us here??

Component Boxes has only one prop – boxes, and it looks like we're passing it the same data every render. It's always the same: red box, wide purple box, yellow box. We have a state variable boxWidthwhich affects the block array, but we don't change it!

That's the problem: Every time React re-renders, we create a completely new array. They are equivalent from the point databut not from the point of view links.

I think it might be helpful to forget about React for a second and talk about good old JavaScript. Let's consider a similar situation:

function getNumbers() {
  return [1, 2, 3];
}
const firstResult = getNumbers();
const secondResult = getNumbers();
console.log(firstResult === secondResult);

What do you think? Is the first result equal to the second?

In a sense, this is true. Both variables have the same structure [1, 2, 3]. But this is not what the operator actually checks ===.

Operator === checks whether two expressions are the same thing.

We have created two different arrays. They may contain the same content, but are not the same array, just like two identical twins are not the same person.

Every time we call a function getNumbers, we create a completely new array, a separate object stored in the computer's memory. If we call it multiple times, we will store multiple copies of this array in memory.

Note that simple data types—such as strings, numbers, and booleans—can be compared by value. But when it comes to arrays and objects, they are only compared by link.

Back to React: Our Component Boxes is also a JavaScript function. When we render it, we call this function:

// Каждый раз, когда мы отображаем этот компонент, мы вызываем эту функцию...
function App() {
  // ...и создаём совершенно новый массив...
  const boxes = [
    { flex: boxWidth, background: 'hsl(345deg 100% 50%)' },
    { flex: 3, background: 'hsl(260deg 100% 40%)' },
    { flex: 1, background: 'hsl(50deg 100% 60%)' },
  ];
  // ...который затем передается в качестве аргумента этому компоненту!
  return (
    <Boxes boxes={boxes} />
  );
}

When the condition name changes, our component re-renders, which re-runs all the code. We are creating a new array boxes and pass it to our component Boxes.

AND Boxes is redrawn because we gave it a completely new array!

Array structure boxes didn't change between renders, but that doesn't matter. All React knows is that it prop boxes received a newly created, never before seen array.

To solve this problem we can use a hook useMemo:

const boxes = React.useMemo(() => {
  return [
    { flex: boxWidth, background: 'hsl(345deg 100% 50%)' },
    { flex: 3, background: 'hsl(260deg 100% 40%)' },
    { flex: 1, background: 'hsl(50deg 100% 60%)' },
  ];
}, [boxWidth]);

Unlike the example with prime numbers, we are not concerned with expensive calculations here. Our only goal is save link to a specific array.

We indicate boxWidth as a dependency because we want the component Boxes re-rendered when the user adjusts the width of the red box.

I think a little sketch will help illustrate this. Previously, we created a new array as part of each snapshot:

However, with the help useMemo we reuse the previously created array of blocks:

By keeping the same reference across multiple renders, we allow pure components to function the way we want, ignoring renders that don't affect the interface.

useCallback hook

Okay, that's all about it UseMemo… and what's about useCallback?

Here's the short version: This is the same thing, but for functions instead of arrays/objects.

Like arrays and objects, functions are compared by reference rather than by value:

const functionOne = function() {
  return 5;
};
const functionTwo = function() {
  return 5;
};
console.log(functionOne === functionTwo); // false

This means that if we define a function inside our components, it will be re-generated on every render, creating an identical but unique function each time.

Let's look at an example:

import React from 'react';

import MegaBoost from './MegaBoost';

function App() {
  const [count, setCount] = React.useState(0);

  function handleMegaBoost() {
    setCount((currentValue) => currentValue + 1234);
  }

  return (
    <>
      Count: {count}
      <button
        onClick={() => {
          setCount(count + 1)
        }}
      >
        Click me!
      </button>
      <MegaBoost handleClick={handleMegaBoost} />
    </>
  );
}

export default App;

This SandBox depicts a typical counter app, but with a special “Mega Boost” button. This button increases the counter significantly, in case you're in a hurry and don't want to press the standard button multiple times.

Component MegaBoost is a pure component due to React.memo. This does not depend on the counter… But it is redrawn whenever the quantity changes!

As we saw with the array boxes, the problem here is that we are generating a completely new function every render. If we render 3 times we will create 3 separate functions handleMegaBoostbreaking through the force field React.memo.

Using what we've learned about useMemowe could solve the problem like this:

const handleMegaBoost = React.useMemo(() => {
  return function() {
    setCount((currentValue) => currentValue + 1234);
  }
}, []);

Instead of returning an array we return function. This function is then stored in a variable handleMegaBoost.

It works… but there is a better way:

const handleMegaBoost = React.useCallback(() => {
  setCount((currentValue) => currentValue + 1234);
}, []);

useCallback serves the same purpose as useMemo, but it's built specifically for functions. We pass it a function directly, and it remembers this function, distributing it between renderers.

In other words, these two expressions have the same effect:

// Это:
React.useCallback(function helloWorld(){}, []);
// ...Функционально эквивалентно этому:
React.useMemo(() => function helloWorld(){}, []);

useCallback is syntactic sugar. It exists solely to make our lives a little more pleasant when remembering callback functions.

When to use these hooks

Okay, we saw how useMemo And useCallback allow us to pass references between multiple renders, reuse complex calculations, or avoid breaking pure components. The question is how often do we need to use them?

In my personal opinion, it would be a waste of time to wrap every single object/array/function in these hooks. In most cases the benefits are negligible; React is highly optimized and re-rendering is often not as slow and expensive as we often think!

The best way to use these hooks is to react to a problem. If you notice that your application is getting a little slow, you can use React Profiler to keep track of slow rendering. In some cases, performance can be improved by restructuring the application. In other cases useMemo And useCallback can help speed up the process.

However, there are several scenarios in which I in advance I use these hooks.

This may change in the future!

The React team is actively researching whether it is possible to “auto-memoize” code at compile time. It's still in the research stage, but early experiments seem promising.

Perhaps in the future all this will be done for us automatically. However, until then we will still have to optimize something ourselves.

Inside custom hooks

One of my favorite little custom hooks is useTogglean assistant that works almost exactly like useStatebut can only switch state between true And false:

function App() {
  const [isDarkMode, toggleDarkMode] = useToggle(false);
  return (
    <button onClick={toggleDarkMode}>
      Toggle color theme
    </button>
  );
}

This is how this hook is defined:

function useToggle(initialValue) {
  const [value, setValue] = React.useState(initialValue);
  const toggle = React.useCallback(() => {
    setValue(v => !v);
  }, []);
  return [value, toggle];
}

Please note that the switching function is remembered using useCallback.

When I create reusable hooks like this, I like to make them as efficient as possible because I don't know where they will be used in the future. In 95% of situations this may be overkill, but if I use this hook 30 or 40 times, there's a good chance it will help improve the performance of my application.

In internal context providers

When we share data in an application using a context, a large object is usually passed as an attribute.

I recommend remembering this object:

const AuthContext = React.createContext({});
function AuthProvider({ user, status, forgotPwLink, children }){
  const memoizedValue = React.useMemo(() => {
    return {
      user,
      status,
      forgotPwLink,
    };
  }, [user, status, forgotPwLink]);
  return (
    <AuthContext.Provider value={memoizedValue}>
      {children}
    </AuthContext.Provider>
  );
}

Why is this beneficial? There may be dozens of pure components using this context. Without useMemo all of these components will be forced to re-render if the parent element AuthProvider will re-render.


Ugh! You have reached the end. I know this lesson covers some pretty heavy ground.

I know that these two hooks are complex, that React itself can seem very complex and confusing. This is a complex instrument!

But here's the thing: If you can overcome the initial barrier, using React will be sheer pleasure.

Similar Posts

Leave a Reply

Your email address will not be published. Required fields are marked *