skip to content

Memoization Tradeoffs

React.memo, useMemo and useCallback are not free: each one adds a comparison and a retained cache entry, so it only pays off when the work it skips exceeds the work it adds. Interviewers ask this to see whether you can name the cases where memoizing makes an app slower.

part ofReactoverview, primer and where to startread it →
on this pageshow

explore

questions

5

In React, a component is wrapped in React.memo. Which re-renders does that actually skip, and which does it not prevent?

level: juniorimportance: must knowfreq 62%

answer

  1. props path only
  2. parent-driven renders
  3. state and context still win
  4. shallow, Object.is per prop
  5. optimization, never a guarantee

basics

~20 s

React.memo skips a re-render caused by the parent, and only when every new prop is the same as the old one by a shallow comparison. Its own state updates, a context change it reads, or any changed prop still re-render it.

solid answer

~50 s

`React.memo(Component)` returns a wrapper that, when the parent re-renders, compares the incoming props with the previous ones prop by prop using `Object.is`. If they all match, React does not call your component function and reuses the previously rendered output, which also spares everything below it. That is the only thing it skips. It does nothing about updates that originate inside the component: a `useState` or `useReducer` update re-renders it, and a context value it reads with `useContext` re-renders it even when props are identical. It also cannot help if a prop is a freshly created object, array or arrow function each render, because those are never equal. And React treats it as an optimization, not a promise — it may re-render a memoized component anyway, so never write logic that depends on the skip happening.

code

javascript · 20 lines
javascript
import { memo, useState } from 'react';

const Row = memo(function Row({ label }) {
  const [open, setOpen] = useState(false);
  return (
    <button onClick={() => setOpen(!open)}>
      {label}: {open ? 'open' : 'closed'}
    </button>
  );
});

export default function List() {
  const [tick, setTick] = useState(0);
  return (
    <div>
      <button onClick={() => setTick(tick + 1)}>tick {tick}</button>
      <Row label="Ada" />
    </div>
  );
}

go deeper

for a junior

Be ready to say that React.memo compares the incoming props and skips the render when they all match, and that state changes inside the component still re-render it.

for a middle

Explain the mechanics: a shallow per-prop Object.is comparison on the parent-driven path, the subtree below skipped along with it, and context subscriptions bypassing the check entirely.

for a senior

Show you have debugged this in production — name inline object, array and arrow props as the reason a memoized component keeps rendering, and read the render reason from the profiler rather than guessing.

for a principal

Own the framing that memo is an optimization React may ignore, so nothing in the codebase's correctness may depend on a skipped render; steer teams toward structural fixes before adding wrappers.

## What React.memo actually is `React.memo` takes a component and returns a new component. The returned component behaves identically, with one addition: when React is about to re-render it *because its parent re-rendered*, React first compares the new props object with the previous one. The comparison is **shallow** — for each key, React asks `Object.is(prevProps[key], nextProps[key])`. If every key matches, React skips calling your function and reuses the element tree it produced last time. ```jsx const Row = memo(function Row({ label }) { return <li>{label}</li>; }); ``` If the parent re-renders a hundred times and always passes `label="Ada"`, the `Row` function body runs once. Because React never re-rendered `Row`, it also never descends into `Row`'s children, so the whole subtree underneath is skipped too. That subtree skip is usually where the real saving is: memoizing a single `<li>` saves almost nothing, memoizing the root of a heavy panel can save a lot. ## The three things it does not prevent **Its own state.** A `useState` or `useReducer` update inside a memoized component re-renders it, exactly like any other component. `React.memo` sits on the props path only; it has no opinion about internal state. This is the misconception juniors most often carry in — that `memo` means "renders once". **Context.** If the component calls `useContext(SomeContext)` and the provider publishes a new value, the component re-renders even when its props are byte-for-byte the same. Context subscriptions bypass the props comparison entirely. **Props that are new every render.** If the parent writes `<Row style={{ color: 'red' }} onSelect={() => pick(id)} />`, both props are freshly allocated objects on every parent render, so `Object.is` reports them unequal and the bailout never fires. The comparison still runs, so you pay a small cost and get nothing. ## It is an optimization, not a guarantee React reserves the right to re-render a memoized component anyway — the React documentation says so explicitly. Treat a skipped render as a performance win you may or may not get, never as a semantic guarantee. Concretely: never put a side effect in a render body and reason "it will only run when props change", and never use `memo` to keep a component from resetting something. If you need something to survive, hold it in state, a ref, or state moved above the component. ## Where the boundary belongs Because `memo` only intercepts the parent-driven path, it is worth placing where a parent re-renders often but the child's inputs rarely move — a static sidebar under a page that re-renders on every keystroke, a row component inside a long list whose row data is stable, a chart fed by a value that changes once a minute. It is close to worthless on a component that renders a few DOM nodes from primitives, because the comparison costs about as much as the render it avoids. ## What to say in an interview State the mechanism in one sentence — shallow prop comparison on the parent-driven render path — then immediately name the escapes: own state, context, and unstable props. Candidates who can list the escapes are signalling they have actually debugged a memoized component that kept re-rendering, which is the thing the question is really probing.

  • If a memoized component re-renders every single time despite unchanged-looking props, what is your first check?
    Look at what the parent passes. An object literal, array literal, arrow function or JSX child created inline in the parent's render is a new reference each time, so the shallow comparison always reports a difference. Check the props one by one in the DevTools Profiler's render-reason list before assuming anything about React itself.
  • Does React.memo do anything for a component that renders because of a context update?
    No. A `useContext` subscription re-renders the consumer whenever the provider's value changes, and the props comparison never gets a chance to bail out. `memo` only intercepts the parent-render path, so the usual fixes are elsewhere: narrow the context, split it, or stop putting a freshly built object in the provider value.
  • Is it safe to rely on React.memo to stop an expensive side effect from running twice?
    No. React documents memoization as a performance optimization it may ignore, so a skipped render is never guaranteed. Side effects belong in an effect with the right dependencies, or in an event handler — not in a render body whose frequency you are trying to control with `memo`.

React.memo is a doorman who checks whether the delivery is identical to yesterday's and turns it away if so. It has no authority over what the people already inside the room decide to do.

saying these in an interview costs you the question

  • Says React.memo makes a component render only once
  • Thinks memo blocks the component's own setState updates
  • Believes memo compares props deeply
  • Assumes memo protects a consumer from context updates
  • Treats a skipped render as a guarantee React must honour

context

open as a page

Both React.memo and useMemo are described as memoization in React, and candidates often use the names interchangeably. What does each one actually cache, and when would you reach for one rather than the other?

level: middleimportance: must knowfreq 72%

basics

~20 s

React.memo caches a component's rendered output and skips re-running it when its props are unchanged. useMemo caches one value inside a component that still re-renders. One avoids a render; the other avoids a computation during a render.

open as a page

A React code review turns up `const total = useMemo(() => price * quantity, [price, quantity]);`. Is that worth writing? What does the useMemo call itself cost on every render?

level: middleimportance: should knowfreq 48%

basics

~20 s

No. On every render React still allocates the arrow function and the dependency array and compares each dependency, which costs about as much as one multiplication. Write the plain expression instead and save useMemo for expensive work or for a reference that must stay stable.

open as a page

React.memo accepts an optional second argument: a function that receives the previous and next props. What must that function return for React to skip the re-render, and what bugs do hand-written comparators commonly introduce?

level: seniorimportance: should knowfreq 38%

basics

~20 s

Return true when the props should be treated as equal, which tells React to skip the re-render — the opposite of the old shouldComponentUpdate convention. Hand-written comparators go wrong by ignoring props that matter, which freezes stale data on screen, or by deep-comparing large objects for more cost than the render saved.

open as a page

A team proposes a codebase-wide rule for a React 19 app: every component wrapped in React.memo and every callback wrapped in useCallback, enforced in code review. How would you evaluate that proposal?

level: principalimportance: nice to knowfreq 30%

basics

~20 s

Reject it as a blanket rule. Memoizing everything adds comparisons, retained caches and dependency arrays across the whole codebase, and most of those wrappers never skip anything. Target memoization at boundaries where profiling shows a real cost.

open as a page