Memoization is just caching with a key you have to get right
Memoization is caching for function calls: remember the result of a call, keyed by its inputs, and return the remembered result when the same inputs come back instead of recomputing. That is the whole idea, and it is genuinely simple. The part that trips everyone is the key — memoization only works if you can decide, cheaply and correctly, whether the inputs are “the same.” Get the comparison wrong and the cache either never hits (so the memo does nothing) or hits when it shouldn’t (so you serve a stale answer). Most broken memoization is a broken key.
A memo is a closure over a cache
At its simplest, memoization is a wrapper that keeps a map from a serialised key to a result. This works when the arguments serialise cleanly to a string:
function memoize(fn) {
const cache = new Map();
return (...args) => {
const key = JSON.stringify(args); // the key: get this right or nothing works
if (cache.has(key)) return cache.get(key);
const result = fn(...args);
cache.set(key, result);
return result;
};
}
const slowSquare = memoize((n) => { /* expensive */ return n * n; });
slowSquare(9); // computes
slowSquare(9); // cache hit — no recompute
Reference equality is the key most frameworks use
JSON.stringify is fine for primitives but wrong for the hot path in a UI, where
inputs are objects and arrays. React’s useMemo and Redux’s reselect compare
inputs by reference (===), not by value — which is fast but means a new
object with the same contents counts as a change and busts the cache:
// BROKEN: `config` is a new object every render, so the memo never hits
const value = useMemo(() => compute(config), [{ mode: "fast" }]);
// FIXED: a stable reference — same object across renders, so the key holds
const config = useRef({ mode: "fast" }).current;
const value = useMemo(() => compute(config), [config]);
This is the number-one reason a useMemo “does nothing”: one of its dependencies
is freshly allocated on every render, so the key never matches and it recomputes
every time — you pay the memo’s overhead and the full cost.
Memoize the expensive and the shared, not everything
Memoization is not free: it holds memory and adds a comparison on every call. It
pays off for genuinely expensive computations, and for derived values shared by
many consumers (a selector feeding twenty components), where one recompute saves
twenty. It is waste on a cheap function called once — the comparison and the retained
reference cost more than the arithmetic you were trying to skip, and now the
garbage collector has more to track for no benefit. Worse, an unbounded cache is
a memory leak: a Map keyed on an ever-changing input grows forever unless you
cap it or evict old entries, so a memo on a high-cardinality key can quietly
consume the heap. The reselect pattern —
compose small memoized selectors so a change to one slice doesn’t recompute
another’s derived data — is where this earns its keep, and it is exactly what the
memoized-selector exercise builds: correct keys, reference-stable inputs, and a
cache that actually hits.