memoize()
intermediateCache function results by argument key — and discuss cache-key strategy and memory trade-offs.
Implement memoizeLast in JavaScript — one cached call, Object.is comparison, and why this exact contract is React's useMemo, memo, and reselect.
Implement
memoizeLast(fn): cache only the previous call — same arguments as last time returns the cached result; different arguments recompute and replace the cache.
This looks like a stripped-down memoize, and mechanically it is — but the single-slot contract is its own idea, and it happens to be the exact semantics of React.useMemo, React.memo, and reselect selectors. Interviewers ask it because it's React's engine in fifteen lines; connect it explicitly and the question becomes a conversation you're leading.
function memoizeLast(fn) {
let lastThis;
let lastArgs = null; // null = "never called" — not an empty array
let lastResult;
return function (...args) {
const hit =
lastArgs !== null &&
lastThis === this && // a different receiver is a different call
args.length === lastArgs.length &&
args.every((arg, i) => Object.is(arg, lastArgs[i]));
if (hit) return lastResult;
lastResult = fn.apply(this, args);
lastArgs = args;
lastThis = this;
return lastResult;
};
}Decisions worth narrating:
Object.is, not === — so a NaN argument can hit the cache (NaN === NaN is false and would force a pointless recompute every call). This is literally React's choice for hook deps — the whole story.lastArgs = null as the never-called sentinel — guarding on "is lastResult set" breaks for functions that return undefined; same flag discipline as once.this too — a.compute(5) then b.compute(5) are different computations if fn reads this; skipping the check silently serves a's answer to b.memoLast({ x: 1 }) twice misses, because each literal is a new object. That's not a bug to fix with deep-equal — it's the contract, and it's the entire reason React demands stable references.let computes = 0;
const area = memoizeLast((w, h) => { computes++; return w * h; });
area(3, 4); // 12 computes=1
area(3, 4); // 12 computes=1 — hit
area(5, 4); // 20 computes=2 — replaced the slot
area(3, 4); // 12 computes=3 — the (3,4) entry is GONE; only ONE slot existsThat last line is the discriminating test: a full memoize would hit; memoizeLast recomputes. If your implementation hits there, you built a multi-entry cache and missed the point.
That trade — hit rate for bounded memory and zero invalidation — is the React rendering model:
// useMemo ≈ memoizeLast keyed on the deps array:
const sorted = useMemo(() => sortItems(items, order), [items, order]);
// deps compared element-wise with Object.is — exactly our `hit` check.
// React.memo ≈ memoizeLast over props (compared key-wise, shallowly).
// reselect selectors ≈ memoizeLast over input-selector outputs.And the infamous pitfall drops out of the contract for free: an inline {} / [] / arrow prop is a new reference each render → the shallow check misses every time → React.memo does nothing. Explaining that via your own fifteen lines is the strongest possible answer to "why didn't memo help?"
A, B, A recomputes A (above); the interviewer is checking you built one slot, not two.undefined returns — cached and served correctly thanks to the lastArgs sentinel.NaN arguments — hit via Object.is; with === you'd recompute every render and never notice.area(config) twice where config.w changed in between: reference-equal → stale hit. Shallow memoization requires immutable update discipline — mutate and it lies to you. This is the deepest one-sentence explanation of why React state must be updated immutably.fn — nothing cached (assignments follow the call), next call retries; also means the slot still holds the previous good entry — subtle and worth saying.Map cache (that's memoize; wrong contract, unbounded memory).=== element comparison with no NaN story.lastResult truthiness/undefined-ness instead of a sentinel.this.{ args, result } with LRU order — and now justify N: for render-shaped workloads the second slot's hit rate is usually ~0, which is why React never added it.memoizeLast(fn, isEqual) parameter — exactly reselect's createSelectorCreator; shallow-compare-props is React.memo's second argument. The API precedents are the answer.useMemo sometimes recompute anyway?" — React reserves the right to drop the cache (concurrent rendering, offscreen); useMemo is a hint, not a guarantee — quoting the docs' stance here is a strong signal.useMemo itself?" — per-hook-slot memoizeLast state stored on the fiber, keyed by call order — which is why the Rules of Hooks exist (call order is the cache key). One sentence, big unlock.Cache function results by argument key — and discuss cache-key strategy and memory trade-offs.
Guarantee a function runs exactly once and returns its cached result forever after.
Pre-fill leading arguments of a function — partial application, and how it differs from currying.
Compose functions left-to-right into a pipeline — the one-liner with a lot of interview depth.