Skip to the chapter
←ReactGGroundwork⚛React
React/Intermediate/I8
19 / 57 written chapters
/

The cover
  1. B1Setup & JSX
  2. B2Thinking in React
  3. B3Components
  4. B4Props
  5. B5useState
  6. B6Events & conditionals
  7. B7Lists & keys
  8. B8Forms
  9. B9useEffect
  10. B10Lifting state & styling
  11. B11Guided project
  12. ★Cheat page
  1. I1useRef
  2. I2Context
  3. I3useReducer
  4. I4Custom hooks
  5. I5Effects in depth
  6. I6Routing
  7. I7State architecture
  8. I8Memoisation
  9. I9Boundaries & portals
  10. I10Data fetching
  11. I11Forms at scale
  12. I12State libraries
  13. I13Auth
  14. I14TanStack Query
  15. I15Testing
  16. I16TypeScript
  17. I17Animation
  18. I18E2E testing
  19. I19i18n
  20. I20File uploads
  21. I21Real-time UI
  22. I22shadcn/ui
  23. ★Cheat page
  1. A1Fiber
  2. A2Concurrent
  3. A3Suspense
  4. A4Server Components
  5. A5Server Actions
  6. A6use()
  7. A7Rendering strategies
  8. A8Effect timing
  9. A9External stores
  10. A10Refs in depth
  11. A11Component patterns
  12. A12Performance
  13. A13Accessibility
  14. A14Design systems
  15. A15Advanced TypeScript
  16. A16Architecture at scale
  17. A17Custom renderers
  18. A18Migrating & upgrading
  19. A19Production debugging
  20. A20Review & judgement
  21. A21State machines
  22. A22Feature flags
  23. A23Interview bank
  24. ★Cheat page

⚛I8 · Part 2 · Intermediate

Memoisation

memo, useMemo and useCallback are a cache — and a cache you did not measure is a cost.

6 min read9 sections4 exercises
19 of 57

In this chapter

  • What actually happens on a re-render
  • The three tools
  • memo, and why it so often does nothing
  • Watch what re-renders
  • useMemo: when it is worth it

and 4 more

about 6 min
09On this page
  1. What actually happens on a re-render
  2. The three tools
  3. memo, and why it so often does nothing
  4. Watch what re-renders
  5. useMemo: when it is worth it
  6. The rule that keeps this sane
  7. Dependency arrays lie the same way here
  8. The comparison is shallow, and you can override it
  9. The React Compiler changes the advice

What actually happens on a re-render

When a component re-renders, React re-renders all of its children by default, regardless of whether their props changed. That sounds alarming and usually is not: rendering is running a function and comparing objects, which is fast. The DOM is only touched where the output differs.

Memoisation is for the cases where that assumption breaks — a genuinely expensive calculation, or a large subtree re-rendering on every keystroke.

The three tools

ToolCachesSkips
useMemo(fn, deps)A computed valueRecomputing it
useCallback(fn, deps)A function identityRecreating it
memo(Component)A component's outputRe-rendering it when props are shallow-equal

useCallback(fn, deps) is exactly useMemo(() => fn, deps). It exists because that pattern is common enough to deserve a name.

memo, and why it so often does nothing

const Row = memo(function Row({ item, onSelect }) { ... });

<Row item={item} onSelect={() => select(item.id)} />   // ✗ memo defeated

memo does a shallow comparison of props. The arrow is a new function on every render, so the comparison always fails and the component re-renders anyway — now with the added cost of the comparison. The same is true of style={{ ... }}, options={[...]}, and any object built inline.

const handleSelect = useCallback((id) => select(id), [select]);
<Row item={item} onSelect={handleSelect} />              // ✓ stable

Which is the real lesson: memo is not one decision, it is a contract with every call site. Miss one prop and the whole thing is inert.

Composition beats memo
// ✗ SlowTree re-renders on every keystroke
function Page() {
  const [q, setQ] = useState("");
  return (<><input value={q} onChange={...} /><SlowTree /></>);
}

// ✓ the state moved down; SlowTree is now a sibling that never re-renders
function Page() {
  return (<><SearchBox /><SlowTree /></>);
}

Moving state down, or passing the expensive subtree in as children, solves the same problem with no cache to maintain and no call sites to keep in line. Try this first, every time.

Watch what re-renders

Which components run when App's state changes?



useMemo: when it is worth it

const sorted = useMemo(
  () => [...rows].sort(compare),
  [rows]
);                                // ✓ 10,000 rows, sorted on every keystroke otherwise

const total = useMemo(() => a + b, [a, b]);   // ✗ the memo costs more than the addition

useMemo is not free: it stores the value, stores the dependency array, and compares it on every render. For arithmetic and short strings the bookkeeping is more expensive than recomputing.

The second legitimate use has nothing to do with speed: referential stability. If a value goes into a dependency array or into a memoised child, memoising it stops that thing running every render.

const value = useMemo(() => ({ user, login }), [user, login]);
<AuthContext value={value}>      // ✓ or every consumer re-renders always

The rule that keeps this sane

  1. Measure first. React DevTools Profiler, with "record why each component rendered" on. Find the component that is actually slow.
  2. Fix the structure. Move state down, pass children, split the component. This removes the problem rather than caching it.
  3. Then memoise, at the specific place the profiler pointed at.

Memoising everything as a habit makes the code harder to read, adds real overhead, and hides the one place it was needed among fifty where it was not.

Dependency arrays lie the same way here

const filtered = useMemo(
  () => rows.filter((r) => r.type === type),
  [rows]                          // ✗ type missing — stale forever
);

A missing dependency in useMemo is worse than in an effect, because it produces a wrong value rather than a skipped side effect, and it is silent.

The comparison is shallow, and you can override it

const Row = memo(Component, (prev, next) => prev.item.id === next.item.id);

Returning true means "equal, skip the render" — the opposite polarity from Array.prototype.sort's comparator, which catches people out. Custom comparators are rarely the right answer; a stable prop usually is.

The React Compiler changes the advice

React Compiler 1.0 shipped on 7 October 2025. It is a build-time plugin that reads your components and hooks, works out which values depend on which, and inserts the memoisation for you. Everything above still explains why a re-render happens; the compiler changes who writes the cache.

npm install --save-dev --save-exact babel-plugin-react-compiler@latest
WhereIs it on?
Expo SDK 54 and laterOn by default in new apps
create-vite, create-next-appOffered as a compiler-enabled template — you choose it
An existing appOff until you add the plugin
React 17 or 18Works, with react-compiler-runtime added as a dependency

What it does better than hand-written memo

function Row({ item, onSelect }) {
  if (!item.visible) return null;
  const label = formatLabel(item);          // the compiler can cache this
  return <li onClick={() => onSelect(item.id)}>{label}</li>;
}

You cannot call useMemo after an early return — it would break the rules of hooks. The compiler is not a hook, so it can memoise conditionally, after the return null, and it caches the inline arrow too. That is the category of optimisation manual memoisation could never reach.

What to do with the useMemo you already wrote

The React team's own guidance is to leave existing memoisation in place, or remove it only with testing, because deleting a useMemo can change what the compiler outputs. For new code, rely on the compiler and keep useMemo/useCallback as an escape hatch for the few places you need exact control — a value that must stay referentially stable for an effect dependency, for example.

Opting in and out, one function at a time

function LegacyChart() {
  "use no memo";                            // skip this one until it is fixed
  ...
}

function Checkout() {
  "use memo";                               // compile this one in annotation mode
  ...
}
compilationModeWhat gets compiled
annotationOnly functions marked "use memo" — the safe way to start on a large app
inferWhat the compiler decides looks like a component or hook; directives override it
allEverything, except functions marked "use no memo"

Either directive can also sit at the top of a module to cover every function in the file. Treat "use no memo" as a temporary marker with a ticket behind it, not a permanent setting.

The compiler needs code that follows the rules

It assumes components are pure and props and state are not mutated. Code that breaks those rules either gets skipped or behaves differently once cached. The recommended config of eslint-plugin-react-hooks now carries compiler-powered rules that catch exactly those patterns:

RuleCatches
set-state-in-renderA setState during render that loops
set-state-in-effectState set synchronously inside an effect — usually a derived value that should be computed during render
refsReading or writing ref.current during render
// eslint.config.js
import reactHooks from "eslint-plugin-react-hooks";
export default [reactHooks.configs.flat.recommended];
Adopting it on an existing app
  1. Turn on the lint rules first and fix what they report. That is most of the work, and it is worth doing even without the compiler.
  2. Add the plugin in annotation mode and mark a few stable, well-tested components with "use memo".
  3. Profile before and after with the React DevTools Profiler — measure, the same way the rest of this chapter says to.
  4. Move to infer, and mark anything that misbehaves "use no memo" until it is fixed.
Interview answer, one sentence

"Re-rendering is usually cheap, so memoisation is a targeted fix rather than a default. memo only helps if every prop is referentially stable, which is why an inline arrow silently defeats it — and moving state down or passing children solves the same problem without a cache to keep in sync. With the React Compiler enabled, it applies that memoisation automatically, so manual memo and useMemo are kept for the cases it cannot see."

04Practice this chapter

Opens in the editor: write it, run it, and check it against real tests.

  • 11 testsWhat React.memo checksintermediate
  • 8 testsA single-argument memoizeintermediate
  • 8 testsMake memo actually workintermediate
  • 5 testsmemo, defeated by a new object every renderintermediate
All 66 for this level →
I8

Finished reading?

0 of 57 chapters read in this series

→Next stop

  1. I7Previous · I7State architecture
  2. I8you are here
  3. Next · I9Error boundaries, portals & keys in depthContaining a crash, escaping the DOM tree, and using keys on purpose.3 min read

01On this page

9 sections · 0%

  1. 01What actually happens on a re-render
  2. 02The three tools
  3. 03memo, and why it so often does nothing
  4. 04Watch what re-renders
  5. 05useMemo: when it is worth it
  6. 06The rule that keeps this sane
  7. 07Dependency arrays lie the same way here
  8. 08The comparison is shallow, and you can override it
  9. 09The React Compiler changes the advice

[] chapters · t top