React Deep-DivesApr 19, 2026·7 min read·

useMemo is a cache, not a guarantee

Every senior React dev reaches for useMemo to 'memoize' a value. The docs explicitly say it isn't memoization — React may throw the cache away at any time, and the list of reasons is longer than you think. Here's the source-anchored story with two interactive canvases.

Pick a scenario and watch the cache misses land — including the ones that happen even when your deps never change:

renders 1 → 8 · useMemo cache state
misses so far: 0
initial
#1
hit
#2
hit
#3
hit
#4
hit
#5
hit
#6
hit
#7
hit
#8
Deps never change. Cache hits every time after the initial render. The mental model most devs have for useMemo.
Pick a scenario. The timeline replays. "miss" states are cases where useMemo recomputed — some are obvious (deps changed), some aren't.

useMemo is the hook everyone learns as "memoization for React". That reading is wrong, and the React docs say so explicitly. useMemo is a cache. React reserves the right to evict that cache for reasons other than a dependency change. If your code relies on the cached value being returned — and relies on it for correctness, not just performance — your code is broken. It just hasn't hit the case that breaks it yet.

This post reads the primary sources (react.dev's useMemo reference) and builds the model you actually need: a short list of cache-miss triggers, and a concrete picture of what happens downstream when a memo cache misses unexpectedly.

tl;dr

useMemo returns a cached value if the dependencies are Object.is-equal to the previous render AND React hasn't thrown the cache away for some other reason. Reasons React may throw it away include: development mode after a file edit, initial mount when the component suspends, and (per the docs) future optimisations. Use useMemo strictly as a performance optimisation. If you need guaranteed identity across renders, useRef or useState is the right tool — they are guaranteed.

The line the docs draw

From the useMemo reference — read the caveat carefully:

React will not throw away the cached value unless there is a specific reason to do that. For example, in development, React throws away the cache when you edit the file of your component. Both in development and in production, React will throw away the cache if your component suspends during the initial mount. In the future, React may add more features that take advantage of throwing away the cache — for example, if React adds built-in support for virtualised lists in the future, it would make sense to throw away the cache for items that scroll out of the virtualised table viewport.

The framing is worth studying. The docs don't say "trust the cache". They say "React won't evict without a reason". Then they list the current reasons, reserve the right to add more, and warn directly:

This should be fine if you rely on useMemo solely as a performance optimisation. Otherwise, a state variable or a ref may be more appropriate.

Two categories of use, cleanly separated:

  • Performance optimisation. You'd rather not recompute expensive(x) on every render; useMemo caches it. If the cache misses, you recompute. The app still works.
  • Semantic guarantee. Your code needs the same reference across renders. Maybe a child's React.memo bails on reference equality. Maybe a useEffect deps array references the memoised value. If the cache misses, React.memo gives up the bail, or the effect fires when it shouldn't.

The second category is a bug waiting for the right scenario. useMemo does not promise the second category. The docs say so directly.

The cache-miss triggers

Three triggers right now, with the door open for more:

  1. Development mode, file edit. You edit a component's source; HMR reloads. Every useMemo inside that component has its cache evicted. Stable deps, no behaviour change — but the ref identity is new.
  2. Suspense during the initial mount. The component suspended on first render. React threw the work away. When the suspension resolved, the cache from the abandoned attempt was gone.
  3. Future optimisations. The docs explicitly mention virtualised lists — an off-screen row in a <VirtualizedTable> is a plausible place to drop cached values. If React ships that, stable deps alone won't keep your memoised value.

The third bullet is the important one philosophically. React reserves the right. It's a team-level commitment from the React team, not an API guarantee.

What this breaks downstream

The most common "useMemo for correctness" pattern I see:

function Parent({ items }) {
  // "Memoised for child's React.memo equality check"
  const config = useMemo(() => ({ sort: "asc", limit: 10 }), []);
  return <List items={items} config={config} />;
}
 
const List = React.memo(function List({ items, config }) {
  // expensive render
});

The intent is a chain:

  • useMemo with stable deps → config keeps the same reference across renders.
  • React.memo compares each prop with Object.isconfig never looks "changed".
  • So List only re-renders when items changes. Parent re-renders that don't change items are skipped.

Now watch what happens when the cache is stable vs. when it isn't:

four parent renders · does the memoized child re-render?
child renders: 1 / 4
render #1
mountfirst render · child mounts
render #2
skippedprop ref === prev · React.memo bails → child skipped
render #3
skippedprop ref === prev · React.memo bails → child skipped
render #4
skippedprop ref === prev · React.memo bails → child skipped
Parent memoizes with stable deps. Reference is retained across renders. Child's props are ===; React.memo bails out. Child skips 3 of 4 renders.
React.memo bails out only when every prop is Object.is-equal to the last render. useMemo's job is to keep that reference stable — but it's a cache, not a guarantee.

Three scenarios; the ladder view shows which of the four parent renders cause the child to re-render too:

  • No useMemo. Fresh object literal every render. React.memo never bails. Child re-renders 3 times.
  • useMemo with stable deps. Ref preserved across renders. Child bails out 3 times.
  • useMemo with a cache-miss event (dev file edit). Ref changes on one render even though deps didn't. Child re-renders on that render — for reasons that never show up in your component's code.

If the app's correctness depends on the List not re-rendering — maybe the child has useEffect with [config] that makes a network call — then the dev-edit scenario is not a theoretical problem. It's a flaky reproduction.

useCallback has the same fine print

useCallback(fn, deps) is defined in terms of useMemo:

useCallback caches a function between re-renders until its dependencies change.

The same caveats apply: React may throw away the function for the same set of reasons, and you should treat it as a performance hint, not as reference-identity bedrock. Passing a useCallback-wrapped handler to a React.memo'd child still breaks if the cache misses on a render that your child's useEffect happens to care about.

When you actually need stable identity

If your code depends on reference identity for correctness, reach for a hook that actually guarantees it:

  • useRef. The returned object is stable for the component's lifetime. Mutate .current freely; React never recreates the ref.
  • useState with an init fn. The initialiser runs once. The state value is stable across renders as long as you don't setState it.
  • Module-scope constants. Anything defined outside the component body has guaranteed identity for the app's lifetime. Prefer these for literal constants the component doesn't own.

These are not "performance optimisations" React reserves the right to invalidate. They are semantics. Use them when semantics is what you need.

Reading your own code

A useful one-question audit for any useMemo/useCallback in your codebase:

If this memo cache missed on a render where the deps were identical, would the app misbehave?

  • "It would be slower for a frame" → legitimate perf use of useMemo. Keep it.
  • "The child in React.memo would re-render once" → legitimate; the child's re-render is the symptom of the miss, not a correctness failure.
  • "A useEffect downstream would fire" → bug. The effect fires on ref inequality; you wrote useMemo expecting ref equality. Move the stable reference to useRef or restructure the effect's deps.
  • "A setState inside a child would run" → bug. Same category; React.memo protected you from a re-render that would otherwise trigger that setState. When the cache misses, the setState fires — your state goes stale.

Most senior React codebases I've audited have a scattering of the last two. They only show up in dev builds after a file edit, or in production under Suspense, and they're vanishingly hard to reproduce. The fix is always the same: stop using useMemo for correctness.

Primary sources