Memoization with React.memo

Memoization with React.memo

Memoization is a technique used in computer science to optimize the performance of functions by caching their previously computed results. In simple terms, memoization stores the output of a function based on its inputs so that if the function is called again with the same inputs, it returns the cached result rather than recomputing the output. This can significantly reduce the time and resources needed to execute a function, especially for functions that are computationally expensive or called frequently. Memoization relies heavily on function purity, which is defined as a function predictably returning the same outputs for given inputs. An example of a pure function .
If the list of todos is large, and the component is re-rendered frequently, this can cause a performance bottleneck in the application. One way to optimize this component is to memoize it using React.memo:

function App() {
  const todos = Array.from({ length: 1000000 });
  const [name, setName] = useState("");

  return (
    <div>
      <input value={name} onChange={(e) => setName(e.target.value)} />
      <TodoList todos={todos} />
    </div>
  );
}

By wrapping the TodoList component with React.memo, React will only re-render the component if its props have changed. Surrounding state changes will not affect it. This means that if the list of todos remains the same, the component will not re-render, and its cached output will be used instead. This can save significant resources and time, especially when the component is complex and the list of todos is large.

Getting Fluent in React.memo

Let’s briefly walk through how React.memo works. When an update happens in React, your component is compared with the results of the vDOM returned from its previous render. If these results are different—i.e, if its props change—the reconciler runs an update effect if the element already exists in the host environment (usually the browser DOM), or a placement effect if it doesn’t. If its props are the same, the component still re-renders and the DOM is still updated.

Scalars (primitive types)

Scalar types, also known as primitive types, are foundational. These types represent singular, indivisible values. Unlike more complex data structures like arrays and objects, scalars do not possess properties or methods, and they are immutable by nature. This means that once a scalar value is set, it cannot be altered without creating a completely new value.

Nonscalars (reference types)

These types don’t store the actual data, but rather a reference or a pointer to where the data is stored in memory. This distinction is crucial because it impacts how these types are compared, manipulated, and interacted with in code. In JavaScript, the most common non-scalar types are objects and arrays. Objects allow us to store structured data with key-value pairs, while arrays provide ordered collections. Functions, too, are considered reference types in JavaScript. A key characteristic of non-scalars is that multiple references can point to the same memory location. This means that modifying data through one reference can impact other references pointing to the same data.

// Scalar types
"a" === "a"; // string; true
3 === 3; // number; true
Symbol.for("this is the best book ever") === Symbol.for("this is the best book ever"); // symbol; true

// Non-scalar types
[1, 2, 3] === [1, 2, 3]; // array; false
{ foo: "bar"} === { foo: "bar" } // object; false

This example underscores the importance of understanding reference comparisons when working with React.memo and non-scalar props. If not used cautiously, one could inadvertently introduce performance issues instead of optimizations.

React.memo often also gets circumvented quite commonly by another non-scalar type: functions. Consider the following case:

<MemoizedAvatar name="Tejas" url="https://github.com/tejasq.png" onChange={() => save()} />

While the props don’t appear to change or depend on enclosing state with props name, url, and onChange all having constant values, if we compare the props we see the following:

"Tejas" === "Tejas"; // <- `name` prop; true
"https://github.com/tejasq.png" === "https://github.com/tejasq.png"; // <- `url` prop; true

(() => save()) === (() => save()); // <- `onChange` prop; false

Once again, this is because we’re comparing functions by reference. Remember as long as props differ, our component will not be memoized. We can combat this by using the useCallback hook inside MemoizedAvatar’s parent:

const Parent = ({ currentUser }) => {
  const onAvatarChange = useCallback(
    (newAvatarUrl) => {
      updateUserModel({ avatarUrl: newAvatarUrl, id: currentUser.id });
    },
    [currentUser]
  );

  return (
    <MemoizedAvatar
      name="Tejas"
      url="https://github.com/tejasq.png"
      onChange={onAvatarChange}
    />
  );
};

Now, we can be confident that onAvatarChange will never change unless one of the things in its dependency array (second argument) changes, like the current user ID. With this, our memoization is fully complete and reliable. This is the recommended way to memoize components that have functions as props.

Great! This now means that our memoized components will never unnecessarily re-render. Right? Wrong! There’s one more thing we need to be aware of.

import React, { useState, useCallback } from "react";

const ExpensiveComponent = React.memo(({ onButtonClick }) => {
  // This component is expensive to render and we want to avoid unnecessary renders
  // We're just simulating something expensive here
  const now = performance.now();
  while (performance.now() - now < 1000) {
    // Artificial delay -- block for 1000ms
  }

  return <button onClick={onButtonClick}>Click Me</button>;
});

const MyComponent = () => {
  const [count, setCount] = useState(0);
  const [otherState, setOtherState] = useState(0);

  // This callback is memoized and will only change if count changes
  const incrementCount = useCallback(() => {
    setCount((prevCount) => prevCount + 1);
  }, []); // Dependency array

  // This state update will cause MyComponent to re-render
  const doSomethingElse = () => {
    setOtherState((s) => s + 1);
  };

  return (
    <div>
      <p>Count: {count}</p>
      <ExpensiveComponent onButtonClick={incrementCount} />
      <button onClick={doSomethingElse}>Do Something Else</button>
    </div>
  );
};

In this example:

  • ExpensiveComponent is a child component that is wrapped in React.memo, which means it will only re-render if its props change. This is a case where you want to avoid passing a new function instance on each render.

  • MyComponent has two pieces of state: count and otherState.

  • incrementCount is a callback that updates count. It is memoized with useCallback, which means the ExpensiveComponent will not re-render when MyComponent re-renders due to a change in otherState.

  • The doSomethingElse function changes otherState but doesn’t need to be memoized with useCallback because it is not passed down to ExpensiveComponent or any other child.

By using useCallback, we ensure that ExpensiveComponent does not re-render unnecessarily when MyComponent re-renders for reasons unrelated to count. This is beneficial in cases where the rendering of the child component is a heavy operation and you want to optimize performance by reducing the number of renders.