Prompt
Implement Reselect's core: createSelector(inputSelectors, combiner):
- Returns
selector(state)that runs each input selector, thencombiner(...inputResults) - If every input result is reference-equal (
===) to last time, return the cached combiner result WITHOUT calling the combiner - Cache size 1 (like Reselect's default)
Solution
Selectors exist because of an uncomfortable fact: state.todos.filter(t => t.done) returns a new array every call, so a component subscribed to it re-renders on every store change — the filter result is equal by value but never by reference. Memoized selectors fix the reference, and this challenge is the entire mechanism in 15 lines.
What to narrate while writing it:
- The cache key is the input references, not the state object. Comparing
state === lastStatewould invalidate on every dispatch (reducers return new roots). Comparing the extracted slices means a dispatch that didn't touchstate.todosproduces the sametodosreference → cache hit → same output reference → subscribed components bail out. The chain "same input refs → skipped combiner → stable output ref" is the whole answer. - This is why immutable updates matter: reference equality is only a meaningful "did it change?" signal if reducers never mutate. Memoized selectors and immutability are two halves of one contract.
- Cache size 1 is a deliberate trade-off: selectors are usually called with the latest state repeatedly, so one slot covers the hot path with zero memory management. The moment one selector instance serves many components with different arguments, you need a factory (or Reselect's
weakMapMemoize) — knowing when size-1 breaks is the senior part. - The combiner-call counter in the tests is exactly how you'd prove a selector works in a unit test — recompute count, not output value.
Red flag: "I'd just useMemo in the component." useMemo memoizes per component instance; a selector memoizes per store slice across all subscribers, and it composes (selectors as inputs to selectors). They solve different layers.
Say it: "The selector caches on input references: unrelated dispatches reuse the slice reference, the combiner is skipped, and the stable output reference is what lets React bail out of re-rendering."