ESC

Type to search the knowledge base.

Memoization

Cache pure function results by argument key — implement memo, handle cache size, and know when React.memo is a different tool.

intermediate3 min read
  • javascript
  • memoization

Expensive pure functions get called with the same inputs again and again — factorial in recursion, derived UI data, coordinate transforms. Memoization stores prior results keyed by arguments and returns the cache hit next time.

Interviewers want a working memo(fn) with a clear cache key strategy — not “use useMemo.” React’s memoization helpers are when to recompute in a render tree. Classic memo is cache function outputs.

Minimal memo

function memo(fn, { keyFn = JSON.stringify, maxSize = Infinity } = {}) {
  const cache = new Map();

  function memoized(...args) {
    const key = keyFn(args);
    if (cache.has(key)) return cache.get(key);

    const result = fn.apply(this, args);
    cache.set(key, result);

    if (cache.size > maxSize) {
      const oldest = cache.keys().next().value;
      cache.delete(oldest); // Map iterates insertion order → crude LRU-ish
    }
    return result;
  }

  memoized.clear = () => cache.clear();
  memoized.cache = cache;
  return memoized;
}
const fib = memo(function f(n) {
  if (n < 2) return n;
  return f(n - 1) + f(n - 2);
});
// Without memo: exponential. With recursive memo: O(n) unique calls.

For recursive cases the inner function must be the memoized one (or look up the same cache). Wrapping only the outer entry point doesn’t help nested calls.

Cache keys (where people mess up)

Key strategy Works for Breaks on
JSON.stringify(args) Primitives, plain objects Functions, undefined order quirks, cyclic structures
First arg only Unary pure fns Multi-arg equality
Custom keyFn Domain ids (user.id) Anything you forget
// Objects with same content, different identity
const cost = memo((opts) => heavy(opts));
cost({ x: 1 }); // miss
cost({ x: 1 }); // hit if JSON key — good for value equality of plain data

Reference equality of object args is not the same as deep equality. Pick the model your callers use.

Bounded cache and invalidation

Unbounded memo on unbounded inputs is a memory leak with extra steps. Cap size (LRU), or clear on:

  • user logout / tenant switch
  • data version bump
  • component unmount (memoized.clear())
const getUser = memo(fetchUserById, { maxSize: 200, keyFn: ([id]) => String(id) });
// after schema migration:
getUser.clear();

What memo does not fix

  • Impure functions — random, Date.now(), DOM reads → stale wrong answers
  • Side-effecting “compute” — caching hides that the effect only ran once
  • Huge result graphs you never reuse — cache overhead without wins

Profile first: if the function is cheap or rarely called with repeats, skip memo.

React adjacent (don’t conflate)

API Role
useMemo(() => f(a), [a]) Skip recompute within a component when deps unchanged
React.memo(Component) Skip re-render when props shallow-equal
Classic memo(fn) Cross-call / cross-component pure function cache

Same idea (don’t redo work), different lifecycle and identity rules.

Interview answer (out loud)

“Memoization caches pure function results by a key derived from arguments. I’d use a Map, a key function (often JSON for plain data), optionally bound the size, and expose clear. It only works if the function is pure and keys match the equality model callers need. In React, useMemo is about render-time recomputation, not a general function cache.”

Further reading

Related guides