Memoization
Cache pure function results by argument key — implement memo, handle cache size, and know when React.memo is a different tool.
- 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,
useMemois about render-time recomputation, not a general function cache.”
Further reading
Related
Related guides
- AbortControllerCancel fetch and other async work with AbortController and AbortSignal — timeouts, race conditions, and cleanup when components unmount.
- Array find, some, every, includesShort-circuiting array predicates: find, findIndex, some, every, and includes — when to use each and common interview traps.
- Array map, filter, reducemap, filter, and reduce as the core transform toolkit — immutability, chaining costs, reduce patterns, and when a plain loop is clearer.
- Arrow Functions Deep DiveLexical this, no arguments object, no construct, concise bodies — when arrows help and when methods and generators need classic functions.
- Async Iteration and for await...ofAsync iterables, for await...of, and streaming data — how async iterators differ from Promise.all and when to use each.