skip to content

In React, what does useMemo actually guarantee about the value it returns between renders, and what must the factory you pass it never do?

level: middleimportance: must knowfreq 62%

answer

  1. hint, not a promise
  2. React may drop what it cached
  3. the factory runs during rendering
  4. must stay correct if recomputed every render
  5. pure factory: no fetch, no setState, no refs

basics

~20 s

useMemo is a performance hint, not a semantic guarantee: React may throw the cached value away and call the factory again even when the dependencies are unchanged. The factory runs during render, so it must be pure — no side effects.

solid answer

~50 s

React describes `useMemo` as a performance optimization rather than a semantic guarantee. While the dependencies compare equal it will normally hand back the previously cached value, but React reserves the right to drop that cache — to free memory, for instance — and simply re-run the factory. So the component has to stay correct on the assumption that the factory could run on every single render; anything whose correctness depends on running once, or on a reference never changing, needs a different tool. The factory also executes during the render phase, which makes purity mandatory: compute from the arguments in scope and return a value, and never fetch, subscribe, mutate the DOM, write to a ref or call a state setter inside it. Work with an observable effect belongs in an event handler or an effect.

code

javascript · 14 lines
javascript
import { useMemo } from 'react';

function Report({ rows, url }) {
  // safe: pure derivation, correct even if it recomputes every render
  const totals = useMemo(
    () => rows.reduce((sum, r) => sum + r.amount, 0),
    [rows]
  );

  // unsafe: I/O during render, re-issued whenever the cache is dropped
  const pending = useMemo(() => fetch(url), [url]);

  return null;
}

go deeper

for a junior

Recall that useMemo is about speed, not behaviour: the code has to work the same if React computes the value again. Know that the factory just computes and returns something.

for a middle

Explain that React may discard the cache and re-run the factory with unchanged dependencies, and that the factory executes during the render phase, which is why it must be pure. Give a concrete example of a side effect that does not belong there.

for a senior

Demonstrate the review instinct: apply the 'would this still be correct if it recomputed every render?' test out loud, and name what you would use instead when the answer is no.

for a principal

Frame it as an architectural boundary — caching may never carry semantics. Be ready to argue why a codebase should treat any correctness dependency on a memo cache as a defect class, not a style preference.

## What the contract actually says The useful mental model is: *while the dependencies compare equal, `useMemo` will probably give you the same value back — and you are not allowed to depend on "probably".* React's own documentation frames the hook as a performance optimization, not a semantic guarantee. That single sentence decides how you are permitted to write the surrounding code. The practical rule that follows is a test you can apply to any `useMemo` call: **if the factory ran on every render, would the component still be correct?** If yes, the hook is being used as intended and only speed changes. If no — if losing the cache would open a second connection, register a duplicate, reset something, or produce a visibly different UI — you have written a correctness dependency on a cache, and it is a bug waiting for the day React drops it. ## Why the cache can be dropped React keeps the cached value on the component's internal work structures, and it treats that memory as reclaimable. It has historically reserved the right to discard cached values in situations such as freeing memory for content that is not visible. You do not get to know when this happens, there is no callback when it does, and it is not something you can observe reliably in development to "prove" it never occurs. The guarantee you can rely on is the negative one: a change in dependencies definitely re-runs the factory. The positive direction — unchanged dependencies definitely reuse the value — is best effort. The cache is also strictly per component instance and per call site. Unmounting the component discards it along with the rest of that instance's state; remounting starts from nothing and calls the factory again. ## Purity: the factory runs during render `useMemo`'s factory is called synchronously in the middle of rendering, before your component returns its output. Rendering in React must be pure, which means the factory inherits the same restriction. Concretely, do not: - start a fetch, open a socket, or subscribe to anything; - mutate the DOM, or read layout in order to write it back; - assign to a ref (`ref.current = ...`) or to a module-level variable; - call a state setter, including "just to sync some derived state"; - mutate an object or array received via props or state. All of those are observable effects, and React may render a component whose output it then throws away, or render at a moment that has nothing to do with what the user just did. An effect performed there happens at an unpredictable time and possibly for a render that never reaches the screen. What *is* fine is pure computation over values already in scope: filtering, sorting a copy, deriving a shape, building a lookup map, constructing a plain object you want to keep referentially stable. ```js // fine - pure derivation over a copy const visible = useMemo( () => rows.filter((r) => r.active).sort((a, b) => a.rank - b.rank), [rows] ); // wrong - a side effect during render const data = useMemo(() => fetch(url).then((r) => r.json()), [url]); ``` Note the second example is wrong twice over: it performs I/O during render, and it would be re-issued whenever the cache is dropped. ## What breaks when the guarantee is assumed Three failure shapes recur: 1. **Resource creation.** Building something that owns an external resource inside the factory means a discarded cache silently creates a second one, and nothing ever tears the first one down — there is no cleanup channel on `useMemo`. 2. **Identity as a protocol.** Code that stores the memoized object as a key, or compares it later to decide whether "the same" thing is still in play, will occasionally see a mismatch after a cache drop and take the wrong branch. 3. **Skipping work by identity.** Using the memo's stable reference as the *only* thing preventing an expensive downstream action means the action fires again unexpectedly when the reference changes. Downstream work must be idempotent or guarded on its own terms. ## The same applies to useCallback `useCallback` is the same cache with a different argument shape, so it carries exactly the same caveat: React may give you a new function identity even when your dependency list did not change. Any code that would malfunction if a callback's identity changed unexpectedly — as opposed to merely doing more work — is relying on a guarantee that does not exist. ## First render, and the cost side On mount the factory always runs; the hook can only save work from the second render onward. And the hook is never free: React stores the dependency array, walks it on every render, and keeps the value alive. That cost is the reason the "is this correct without the cache?" test matters — you should be able to delete a `useMemo` and get a slower but still correct component.

  • Does the same 'no semantic guarantee' caveat apply to useCallback?
    Yes — useCallback is the same cache with a different argument shape, so React may hand back a new function identity even when the dependency list is unchanged. Any logic that would break, rather than merely do extra work, when a callback's identity changes is relying on a guarantee React never made.
  • Where does work belong if it must happen once when a value changes, and not on every render?
    In an effect, keyed on that value, with a cleanup for whatever it created; or in the event handler that caused the change, if a user action is the trigger. Those are the places React defines a running-order and a teardown for. useMemo offers neither a cleanup nor a run-once promise.
  • Is a useMemo cache shared between two mounted instances of the same component?
    No. Each hook call site keeps its own cell on its own component instance, so two instances memoize independently, and neither can see the other's cached value. Unmounting discards the cell with the rest of that instance's state, so a remount recomputes from scratch.

saying these in an interview costs you the question

  • Says useMemo guarantees the factory runs only when dependencies change
  • Puts a fetch, subscription or timer inside the memo factory
  • Calls a state setter inside useMemo to keep derived state in sync
  • Assumes the cached value survives the component unmounting
  • Treats a memoized reference as an identity other code can rely on forever

context