Implement a polyfill for useMemo
`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.
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
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.
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(whereNaN === NaNis false).Object.is(+0, -0) === false(where+0 === -0is 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:
- Skipping expensive computation when inputs haven't changed.
const sorted = useMemo(() => [...items].sort(by(complexCriteria)), [items]);- Stable referential identity for objects / arrays passed down.
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
const result = useMemo(() => process(data, filter), [data]);
// BUG: filter not in deps; stale `filter` used after it changesThe 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])`.