skip to content

Why React Memoisation Usually Doesn't Work

Why React memoisation rarely pays off unless everything is memoised, how referential stability breaks useMemo/useCallback.


· 4 min read

Last Updated:


Introduction

In React, a component re-renders in response to one of three conditions:

  • It’s parent has re-rendered.
  • It’s local state has updated.
  • It’s context, or the state within the provider, has updated.

Sometimes, the cost to re-render can be expensive and be the cause for a performance bottleneck. This is a problem that memoisation can solve.

The All-Or-Nothing Problem

For memoisation to be effective in a React application, an all-or-nothing approach must be taken. Memoisation only works when every single non-primitive prop and the component itself are memoized. Everything else is just a waste of memory and unnecessarily complicates your code.

This constraint is why memoisation, through use of useMemo and useCallback hooks, is an ineffective and largely unrecommended technique.

Lets have a look at some examples.

Broken Memoisation Example

unmemoised-component.tsx
function Library({ books }: { books: Array<string> }) {
const reserveBook = useCallback(() => {
// some operation for reserving a book
}, [books]);
return <LibraryCheckout reserveBook={reserveBook} />;
}

Job done, right? Not quite. There are actually two problems here that mean we have not achieved proper, effective memoisation.

  1. The LibraryCheckout component isn’t memoised, so it will re-render every time Library re-renders, regardless of our useCallback.
  2. The books prop might not be referentially stable. We have no confidence that the books prop we have passed as an internal dependency to our useCallback is referentially stable. If it isn’t memoised itself, the useCallback hook is contributing nothing because it will be re-run each time books updates. React uses Object.is to compare dependency array props. In order for the reserveBook callback to remain memoised, books must also be memoised because it is not a primitive and therefore redeclaring it can change its reference.

1 is an easy solve. We just wrap the component in .memo. React will skip rendering that component (and its children) if its props are unchanged.

memoised-component.tsx
import { memo } from "react";
function Library({ books }: { books: Array<string> }) {
const reserveBook = useCallback(() => {
// some operation for reserving a book
}, [books]);
return <MemoisedLibraryCheckout reserveBook={reserveBook} />;
}
function LibraryCheckout({ reserveBook }: { reserveBook: () => void }) {...};
function MemoisedLibraryCheckout = memo(LibraryCheckout);

2 is much harder. It requires us to climb the component tree, adventure beyond the scope of the component we are actually concerned with, to see whether or not our books prop is properly memoised. This is a great illustration of why memoisation is a flunky technique.

To foot stamp the ineffectiveness of this type of memoisation, what if books itself was itself the product of another potentially referentially unstable prop? We’d have to walk further up the tree, further from our initial point of inspection and point of memoisation. We are forced to travel an indeterminate memoisation chain.

memoised-component-unmemoised-prop.tsx
function City() {
// ⚠️ We'd have to memoise books.
const books = ["The Great Gatsby", "1984", "To Kill a Mockingbird"];
return (
<Library books={books} />
)
}
// ⚠️ We'd have to "memo" the Library component.
function Library({ books }: { books: Array<string> }) {
const reserveBook = useCallback(() => {
// some operation for reserving a book
}, [books]);
return <MemoisedLibraryCheckout reserveBook={reserveBook} />;
}

The job to achieve effective, workable memoisation is completely indeterminate. And if anyone makes a change to the props passed to any other these components without considering memoisation then all this effort is wasted. Memoisation is incredibly fragile and it is easy to break in the future. It requires you to understand context and rules far beyond the component you are actively coding in.

References

Why we memo all the things The all-or-nothing cost of using Reacts idiomatic solution for memoisation.

How to write performant React code Nadia produces some of the best React performance related content on the Interwebs.

The useless useCallback This triggered me to write this. Dominique does a good job of highlighting the fragile and indeterminate memoisation chain.