Back to React
React
easy
mid

How would you implement a polyfill for the useMemo hook?

`useMemo(factory, deps)` runs `factory()` on first render and again only when deps change (by `Object.is`); otherwise returns the cached value. Polyfill: store `{ value, deps }` in the component's hook slot array; on render, compare deps to previous; reuse or recompute.

4 min read·~20 min to think through

useMemo caches a value across renders as long as its deps don't change. It's the engine that useCallback is a thin wrapper around.

Behavior

js
const expensiveValue = useMemo(() => computeHeavy(items), [items]);
  • First render: call factory, cache value with deps.
  • Subsequent: if deps are referentially equal by Object.is, return cached value; otherwise recompute.
  • No deps array → recompute every render (essentially useless).
  • Empty [] → compute once on mount, return forever.

The polyfill

Hooks live in a per-component ordered slot list, advanced by an index on each render.

js
let currentComponent = null;

function useMemo(factory, deps) {
  const c = currentComponent;
  const i = c.hookIndex++;
  const prev = c.hooks[i];

  if (prev && depsEqual(prev.deps, deps)) {
    return prev.value;
  }
  const value = factory();
  c.hooks[i] = { value, deps };
  return value;
}

function depsEqual(a, b) {
  if (!a || !b || a.length !== b.length) return false;
  for (let i = 0; i < a.length; i++) {
    if (!Object.is(a[i], b[i])) return false;
  }
  return true;
}

Why Object.is

Stricter than === in two cases:

  • Object.is(NaN, NaN) === true (where NaN === NaN is false).
  • Object.is(+0, -0) === false (where +0 === -0 is true).

For React's dependency comparison, those edges rarely matter, but Object.is is the documented contract.

When does useMemo actually help?

Two real reasons:

  1. Skipping expensive computation when inputs haven't changed.
js
const sorted = useMemo(() => [...items].sort(by(complexCriteria)), [items]);
  1. Stable referential identity for objects / arrays passed down.
js
const value = useMemo(() => ({ theme, locale }), [theme, locale]);
<Ctx.Provider value={value}>  {/* now stable across renders if deps unchanged */}

When useMemo is overhead

  • Computing a primitive like a + b — the memo machinery itself costs more.
  • The factory is cheap.
  • Nothing downstream depends on identity stability.

React isn't a free lunch dispenser — every useMemo is a slot, a deps comparison, and a potential garbage object. Use it where it earns its keep.

Garbage collection caveat

React may discard the cached value under memory pressure (mentioned in docs). So useMemo is a hint, not a guarantee. For correctness-critical caching, use useState or a ref.

The stale-deps trap

jsx
const result = useMemo(() => process(data, filter), [data]);
// BUG: filter not in deps; stale `filter` used after it changes

The ESLint rule react-hooks/exhaustive-deps catches these.

Interview framing

"useMemo stores { value, deps } in the component's hook slot. On render, compare deps to the previous slot by Object.is; if equal, return the cached value; else call the factory and store. useCallback is literally useMemo(() => fn, deps). Two legitimate uses: skipping an expensive computation, and stabilizing an object/array reference for memoized children or context values. For cheap computations it's pure overhead — and React may discard the cache under memory pressure, so it's a hint, not a guarantee."

Follow-up questions

  • Why Object.is for deps comparison?
  • When is useMemo overhead instead of optimization?
  • What's the relationship between useMemo and useCallback?
  • Why might React discard the memoized value?

Common mistakes

  • Wrapping every primitive computation in useMemo.
  • Missing deps — stale values.
  • Treating useMemo as a guarantee instead of a hint.

Performance considerations

  • Memo machinery is cheap but not free. Net win iff factory is expensive or referential stability is consumed downstream.

Edge cases

  • Factory throws — not memoized; re-runs next render.
  • Hook-order violation breaks slot indexing.
  • Mutating the memoized value externally — React doesn't know.

Real-world examples

  • Memoizing sort/filter results on large arrays.
  • Stable context Provider value: `useMemo(() => ({ ... }), [a, b])`.

Senior engineer discussion

Seniors apply useMemo where it earns its keep — expensive compute or downstream identity stability — and don't apply it reflexively. They know it's a hint, not a guarantee, and respect the exhaustive-deps rule.

Related questions