TypeOfNaN

How does the useMemo hook work in React?

Nick Scialli June 08, 2021🚀🚀 7 minute read

The useMemo hook in React is actually one of my favorite hooks—it enables a lot of best practices and solves some potentially tricky bugs when writing React components.

How it works

The useMemo hook is a function that takes two arguments: a callback function and a dependency array:

function MyComponent() {
  const myValue = useMemo(
    () => {
      /* this is the callback function */
    },
    [
      /* this is the dependency array */
    ]
  );

  return <>Here's the value: {myValue}!</>;
}

A more realistic version of the interface might look like this:

function MyComponent({ a, b, c }) {
  const myValue = useMemo(() => {
    return computeSomething(a, b, c);
  }, [a, b, c]);

  return <>Here's the value: {myValue}!</>;
}

But wait, why not just directly assign computeSomething(a, b, c) to myValue? Couldn’t it look like this?

function MyComponent({ a, b, c }) {
  const myValue = computeSomething(a, b, c);

  return <>Here's the value: {myValue}!</>;
}

The answer, as with many things in programming, is maybe.

What does useMemo solve?

When you remove the useMemo hook, computeSomething is run every time the component renders. That might be okay for simple functions, but it could be a performance hit if computeSomething is expensive. Additionally, this has implications for referential equality, which becomes important when we need some assurance that myValue is the same as the last time the component was rendered.

Let’s first talk about preventing expensive recomputes and next discuss referential equality.

Preventing expensive recomputes

Let’s say our computeSomething function is particularly expensive. Maybe it loops through a bunch of elements multiple times. At any rate, computeSomething is a function of a, b, and c. Assuming it’s a pure function (i.e., it has no side effects and will return the same output for the same inputs), then there should be absolutely no reason to recompute myValue unless a, b, or c changes. This is exactly what the useMemo hook does! It will return a memoized value from the provided function unless one of the values in the dependency array changes.

A quick example of avoiding recomputes

In the following code snippet, I have made our component just a little more complex. Now, it has a count state that we’re able to increment using a button.

function MyComponent({ a, b, c }) {
  const [count, setCount] = useState(0);

  const myValue = useMemo(() => {
    return computeSomething(a, b, c);
  }, [a, b, c]);

  return (
    <>
      Here's the value: {myValue}!
      <button
        onClick={() => {
          setCount(count + 1);
        }}
      >
        Increment
      </button>
    </>
  );
}

Note that computeSomething doesn’t rely on the count variable, so it would make no sense to recompute it when count changes. But we’re good—with useMemo, we don’t recompute the value unless something in the dependency array changes.

Referential equality

Lack of referential equality is probably the more frequently encountered issue when it comes to recomputed values. Let’s say we have a function called makeObject that takes a, b, and c as arguments and returns an object:

function makeObject(a, b, c) {
  return { a, b, c };
}

function MyComponent({ a, b, c }) {
  const myObject = makeObject(a, b, c);

  return <>Here's the object: {JSON.stringify(myObject)}</>;
}

It’s important to remember that on each render of the component, makeObject will return a new object with a new reference in memory. React relies on referential equality for quite a bit of rendering logic. Just as a refresher, objects with the same properties are not equal due to lack of referntial equality: they reference two different objects in memory. For example, the following is false.

{a : 1} === {a: 1};
// false

Back to our useMemo discussion—We’re going to throw our count button back in here as well as a new component:

function makeObject(a, b, c) {
  return { a, b, c };
}

function MyComponent({ a, b, c }) {
  const [count, setCount] = useState(0);
  const myObject = makeObject(a, b, c);

  return (
    <>
      <SomeComponent obj={myObject} />
      <button
        onClick={() => {
          setCount(count + 1);
        }}
      >
        Increment
      </button>
    </>
  );
}

If we click the Increment button, we increment our count, we re-run makeObject (needlessly!), and then we re-render SomeComponent because myObject now points to a different object in memory than it did the previous render! This might not be a big deal for a lot of scenarios, but if you’re hitting performance issues, you might want to considering memoizing the makeObject call.

function makeObject(a, b, c) {
  return { a, b, c };
}

function MyComponent({ a, b, c }) {
  const [count, setCount] = useState(0);
  const myObject = useMemo(() => makeObject(a, b, c), [a, b, c]);

  return (
    <>
      <SomeComponent obj={myObject} />
      <button
        onClick={() => {
          setCount(count + 1);
        }}
      >
        Increment
      </button>
    </>
  );
}

Where this becomes a big deal: dependency arrays

In our previous scenario, we saw that you could hit performance issues from rendering a component too many times. While this is true, it’s often not something you need to worry about until you’re actually hitting performance problems.

What will affect you, however, is referential equality issues within dependency arrays. I see this a lot with useEffect dependency arrays. Consider the following code:

function makeObject(a, b, c) {
  return { a, b, c };
}

function MyComponent({ a, b, c }) {
  const [count, setCount] = useState(0);
  const myObject = makeObject(a, b, c);

  useEffect(() => {
    doSomething(myObject);
  }, [myObject]);

  return (
    <>
      Here's the object: {JSON.stringify(myObject)}
      <button
        onClick={() => {
          setCount(count + 1);
        }}
      >
        Increment
      </button>
    </>
  );
}

We now have a situation where incrementing the count will run the useEffect hook since myObject will be needlessly recreated as a new object in memory. This could lead to all sorts of undesired results! Depending on what doSomething is, it could also result in an infinite effect loop, yikes!

Again, the solution to this problem is a useMemo hook to memoize the return value of our function:

function makeObject(a, b, c) {
  return { a, b, c };
}

function MyComponent({ a, b, c }) {
  const [count, setCount] = useState(0);
  const myObject = useMemo(() => makeObject(a, b, c), [a, b, c]);

  useEffect(() => {
    doSomething(myObject);
  }, [myObject]);

  return (
    <>
      Here's the object: {JSON.stringify(myObject)}
      <button
        onClick={() => {
          setCount(count + 1);
        }}
      >
        Increment
      </button>
    </>
  );
}

Conclusion

I hope this post helped you understand the many reasons to invest in the useMemo hook!

If you'd like to support this blog by buying me a coffee I'd really appreciate it!

Nick Scialli is a software engineer at the U.S. Digital Service.