C.W.K.
Stream
Lesson 05 of 06 · published

When Manual memo Still Earns Its Keep

~10 min · usememo, react.memo, edge-cases

Level 0React Novice
0 XP0/54 lessons0/12 achievements
0/100 XP to next level100 XP to go0% complete
The Compiler covers 95% of the memoization you used to write by hand. The remaining 5% is worth knowing — that's where careful application of useMemo, useCallback, and React.memo still helps.

Case 1 — referential identity for non-React APIs

You pass a callback to a Web Component (or Three.js, or a Web Worker bridge) that compares props by identity. The Compiler memoizes for React's reconciler — but the external API has its own equality check. Manual useCallback guarantees the reference is stable in a way the external library expects.

Case 2 — expensive pure computations

The Compiler memoizes values whose inputs are React-tracked (props, state). For a value computed from external/imperative sources (a DOM measurement, a Date.now(), an imported lookup table), wrap it in useMemo manually if the computation is genuinely expensive. The Compiler isn't conservative; it's just narrow.

Case 3 — React.memo on list items

A long list where each row is moderately expensive: wrapping Row in React.memo means individual rows don't re-render when sibling rows update. The Compiler can sometimes infer this, but explicit React.memo is still the clearest signal — and works regardless of Compiler enablement.

Case 4 — Compiler isn't enabled

Not every project has the Compiler turned on. If you're contributing to a codebase that doesn't, manual memoization is the only tool you have. Match the codebase's style.

Manual memo is now a precision tool, not a default reflex. Before the Compiler: wrap everything just in case. With the Compiler: wrap only when you have a specific reason — a non-React API, a profiled hot spot, a list of expensive rows, a codebase without the Compiler. The default has flipped.

Code

List item with React.memo — still useful·tsx
import { memo } from "react";

type Item = { id: string; label: string };

// Without memo: every parent re-render re-renders every Row, even unchanged ones.
// With memo: Row re-renders only when its specific props change.
const Row = memo(function Row({ item, onClick }: {
  item: Item;
  onClick: (id: string) => void;
}) {
  return (
    <li onClick={() => onClick(item.id)} className="px-3 py-2 hover:bg-bg-elevated">
      {item.label}
    </li>
  );
});

function List({ items, onItemClick }: { items: Item[]; onItemClick: (id: string) => void }) {
  return (
    <ul>
      {items.map((item) => <Row key={item.id} item={item} onClick={onItemClick} />)}
    </ul>
  );
}

// (With Compiler enabled, onItemClick is stable automatically — but React.memo
// still earns its keep on the row level for large lists.)

External links

Exercise

Build a list of 500 items where each row has a click handler. Without React.memo, observe in the DevTools Profiler how clicking one row re-renders all rows (because the parent re-renders). Wrap the row in React.memo. Re-profile and confirm only the clicked row re-renders. Then enable the Compiler and observe whether it helps or whether React.memo is still earning its keep.
Hint
The Compiler helps with callback stability (so memo's prop comparison succeeds), but the memo wrapper is what prevents the child from re-rendering when props are unchanged. They compose.

Progress

Progress is local-only — sign in to sync across devices.
Spotted a bug or have feedback on this page?Report an Issue

Comments 0

🔔 Reply notifications (sign in)
Sign inPlease sign in to comment.

No comments yet — be the first.