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:
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.
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
useMemosolely 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;useMemocaches 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.memobails on reference equality. Maybe auseEffectdeps array references the memoised value. If the cache misses,React.memogives 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:
- Development mode, file edit. You edit a component's source; HMR reloads. Every
useMemoinside that component has its cache evicted. Stable deps, no behaviour change — but the ref identity is new. - 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.
- 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:
useMemowith stable deps →configkeeps the same reference across renders.React.memocompares each prop withObject.is→confignever looks "changed".- So
Listonly re-renders whenitemschanges. Parent re-renders that don't changeitemsare skipped.
Now watch what happens when the cache is stable vs. when it isn't:
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.memonever bails. Child re-renders 3 times. useMemowith stable deps. Ref preserved across renders. Child bails out 3 times.useMemowith 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:
useCallbackcaches 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.currentfreely; React never recreates the ref.useStatewith 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.memowould re-render once" → legitimate; the child's re-render is the symptom of the miss, not a correctness failure. - "A
useEffectdownstream would fire" → bug. The effect fires on ref inequality; you wroteuseMemoexpecting ref equality. Move the stable reference touseRefor 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
- react.dev —
useMemo— the cache framing, the caveat paragraph, the three current eviction triggers. - react.dev —
useCallback— defined in terms ofuseMemo; inherits the same caveats. - react.dev —
React.memo— the bail-out behaviour that downstream consumers of memoised values rely on; confirmsObject.iscomparison. - react.dev —
useRef— the actually-guaranteed-stable alternative when you need reference identity for correctness.