React

React.memo: when it actually helps

React.memo skips the re-render when props stay equal, but it pays only on expensive components with stable props: elsewhere it adds comparisons without gain.

Key points

  • React.memo compares each prop with Object.is and skips the render when all of them match.
  • An object or a function created in the parent's render defeats the comparison on every pass.
  • memo does not stop re-renders caused by internal state or by a context that changes.

React.memo wraps a component and skips the render when its props are equal. The comparison is shallow, so it pays on expensive components with stable primitive props, and adds work everywhere else.

What it really compares

The comparison is Object.is on each prop, one by one. Equal numbers, strings and booleans stop the render; an object created in the parent's render gets a new identity on every pass, so the comparison always fails and the component re-renders anyway.

The case where it pays

A table with fifty rows and a search field above it: every keystroke re-renders the parent. When the row is memoised and receives only id and label, the rows that did not change are not recomputed.

const Row = memo(function Row({ id, label, onSelect }) {
  return (
     onSelect(id)}>
      {label}
    
  );
});

The onSelect function is a prop like any other: when it is created in the parent's render it changes identity on every pass and cancels the memo. Stabilise it with useCallback, or pass it through a context that rarely changes.

The case where it does not pay

  • Unstable props: objects, arrays and functions created during render defeat the comparison, and the memo becomes pure cost.
  • Cheap components: a cell holding one string costs less than the comparison and the memory the memo needs.
  • Context: memo does not stop re-renders caused by a context that changes; those arrive regardless.

Measure before adding it

npm create vite@latest memo-demo -- --template react-ts
cd memo-demo
npm install
npm run dev

With the app open, the React DevTools Profiler records renders per component and shows how long each one takes. Add memo only to the component at the top of that list with a measurable time, not to everything that looks slow.

Live records in the catalogue

Two records suited to the test: table for a grid with many rows and list-item for a reusable row. The full catalogue sits among the 111,985 components and the 12,808 icons.

The limitation

memo stops only the renders that come from the parent. Internal state that changes, a context that changes or an unstable prop re-render the component anyway; and the comparison is not free, so on small components the cure costs more than the problem.

Where to continue

The stable function the memo needs is in useCallback; the case where the memo does not help is in context and performance; the comparison with a framework that needs no manual memo is in Svelte vs React.

Frequently asked questions

Is React.memo the same as useMemo?

No. memo wraps a component and skips its render when props do not change; useMemo keeps the result of a calculation inside a component. They solve different problems.

When is the shallow comparison not enough?

When a prop is an object with many fields the parent cannot stabilise. You can pass a comparison function to memo, but a deep comparison costs as much as the render you want to avoid.

Does memo work with class components?

Yes. memo accepts any component, class or function, and returns a component with the prop comparison. It does not work on values: for those you want useMemo.

Do I still need memo with the React compiler?

Much less. The compiler inserts automatic memoisation where it finds a gain, so a hand-written memo helps only after a measurement shows a component that is still slow.

Why does my memoised component keep re-rendering?

Almost always because of a prop with a new identity on every render, or a context that changes. Check the props in the Profiler and find the values recreated in the parent.