A memoized selector derives a value from state and recomputes only when the specific slices of state it reads change by reference. This is the core idea behind reselect, the standard memoization layer for Redux selectors: createSelector takes any number of cheap input selectors plus one result function, and it reruns the result function only when one of the input values changes — otherwise it hands back the previously cached result. createStructuredSelector composes a map of selectors into one selector that yields an object of their results, memoized the same way.
Build reselectSelectors, an object exposing createSelector and createStructuredSelector, plus a recomputations() counter on every selector so you can prove the cache is being reused.
type Selector = ((state: any, ...args: any[]) => any) & {
recomputations(): number;
};
const reselectSelectors: {
// (...inputSelectors, resultFn) -> a memoized selector
createSelector(...args: Function[]): Selector;
// { key: selector, ... } -> a selector returning { key: result, ... }
createStructuredSelector(map: Record<string, Function>): Selector;
};
const selectTotal = reselectSelectors.createSelector(
(state) => state.cart.items,
(items) => items.reduce((sum, item) => sum + item.price, 0),
);
const state = { cart: { items: [{ price: 10 }, { price: 5 }] } };
selectTotal(state); // 15 — runs the result function
selectTotal(state); // 15 — same items reference, returns the cached 15
selectTotal.recomputations(); // 1
const selectView = reselectSelectors.createStructuredSelector({
total: (state) => state.total,
user: (state) => state.user,
});
const r1 = selectView({ total: 42, user: { name: 'Ada' } });
// { total: 42, user: { name: 'Ada' } }
selectView.recomputations(); // 1
===. Immutable updates give a changed slice a new reference; an unchanged slice keeps its old one.recomputations() — returns how many times the result function ran, so a test can assert the cache was reused across calls.You will build createSelector, which wraps an expensive derivation so it only reruns when the exact pieces of state it reads change by reference, and createStructuredSelector, which composes several selectors into one object result.
A selector turns state into a derived value: a filtered list, a cart total, a formatted view model. Recomputing it is often expensive, and in a Redux app the same selector runs after every dispatched action, even actions that touched a completely unrelated slice of state. You want the selector to notice when its inputs are unchanged and hand back the exact same result it produced last time, so a downstream React.memo component or a useMemo can skip its work too. That is what reselect does: cheap input selectors pull out the slices you depend on, and one result function runs only when those slices actually change.
Think of a selector as a two-stage pipeline. The first stage is a set of cheap input selectors that pluck the slices you care about out of state. The second stage is one result function that turns those slices into the derived value, and it is the only expensive part. Between the two stages sits a gate: it compares this run's input values to the previous run's, slot by slot, with ===. If every slot matches, the gate short-circuits and returns the cached result without ever calling the result function.
The obvious version just runs the whole thing on every call:
function createSelector(...inputSelectors) {
const resultFn = inputSelectors[inputSelectors.length - 1];
const inputs = inputSelectors.slice(0, -1);
return function selector(state) {
const values = inputs.map((fn) => fn(state));
return resultFn(...values); // always recompute — no cache
};
}
It returns the right value, so it looks done. The trouble is it produces a brand-new result on every call. If the result function builds an array with items.map(...), each call returns a different array reference even when items never changed. Every downstream React.memo or useMemo that compares by reference now sees a new value and re-renders, which is the exact work a selector is meant to prevent.
The tempting over-correction is to cache the inputs and compare them with a deep equality check. That is correct, but it walks the entire input structure on every call, which is O(n) — for a large list you pay a full deep comparison every time, often more than the derivation you were trying to skip. Because Redux updates are immutable, a reference check gets the same answer for free: a real change always produces a new reference, and an unchanged slice always keeps its old one.
Keep the last inputs and last result in the closure. On each call, run the input selectors, compare their values to last time by reference, and rerun the result function only when something actually changed.
function createSelector(...args) {
// The last argument is the result function; the rest are input selectors.
const resultFn = args[args.length - 1];
const inputSelectors = args.slice(0, -1);
let lastInputs = null; // the input VALUES from the previous run, or null before the first run
let lastResult; // the cached output of resultFn
let recomputations = 0;
function selector(state, ...extraArgs) {
// Input selectors are cheap and always run; extraArgs lets a selector read a prop, e.g. an id.
const inputs = inputSelectors.map((input) => input(state, ...extraArgs));
// Compare this run's values to last run's, slot by slot, by reference.
const changed =
lastInputs === null ||
inputs.length !== lastInputs.length ||
inputs.some((value, i) => value !== lastInputs[i]);
if (changed) {
// A new reference in at least one slot: recompute and cache.
lastResult = resultFn(...inputs);
recomputations += 1;
lastInputs = inputs;
}
// Otherwise fall through and return the exact same result object as last time.
return lastResult;
}
selector.recomputations = () => recomputations;
return selector;
}
function createStructuredSelector(selectorMap) {
const keys = Object.keys(selectorMap);
const selectors = keys.map((key) => selectorMap[key]);
// Reuse createSelector: the members become the input selectors, and the
// result function stitches their values back into an object under the keys.
return createSelector(...selectors, (...values) => {
const result = {};
keys.forEach((key, i) => {
result[key] = values[i];
});
return result;
});
}
const reselectSelectors = { createSelector, createStructuredSelector };
module.exports = { reselectSelectors };
The whole cache is two closure variables: lastInputs (an array of the values the input selectors produced) and lastResult. The changed check treats the very first call, a different input count, or any slot that fails === as a change; everything else is a cache hit that returns lastResult untouched, so the result keeps its reference across calls. createStructuredSelector does no new memoization of its own: it hands the member selectors to createSelector as inputs and lets that one cache do the work, so the assembled object is rebuilt only when a member value changes.
Take a selector for the visible todos and trace four calls:
const selectVisible = reselectSelectors.createSelector(
(state) => state.todos,
(state) => state.filter,
(todos, filter) => todos.filter((t) => t.status === filter),
);
selectVisible(state) with state.filter of 'active'. lastInputs is null, so the gate reports a change. The result function runs, filters the list, caches the array, and recomputations becomes 1.state.todos and state.filter are the same references. inputs is [todos, 'active'], matching last run slot for slot, so the gate short-circuits: it returns the same filtered array and recomputations stays 1.todos array. Now state.todos is a new reference; slot 0 fails ===, the result function reruns, a fresh filtered array is cached, and recomputations becomes 2.recomputations stays 2.state.todos.push(todo) keeps the same array reference, so the selector never notices and serves a stale result. Fix: replace the array (state.todos = [...state.todos, todo]), the Redux rule.(s) => ({ ...s.user }) returns a fresh object every run, so the reference check always fails and you recompute every time, silently losing all memoization. Fix: input selectors should return existing references straight out of state.=== for a custom equalityFn such as a shallow-equal, useful when an input selector unavoidably returns a new array each run. This is what createSelectorCreator configures.n cache (an LRU keyed by inputs, or a WeakMap keyed by the arguments) fixes the alternating-input thrash, which modern reselect exposes through weakMapMemoize and a maxSize option.Keep practising the same patterns with a nearby challenge.
No submissions yet
Share your approach and start the discussion.
A memoized selector derives a value from state and recomputes only when the specific slices of state it reads change by reference. This is the core idea behind reselect, the standard memoization layer for Redux selectors: createSelector takes any number of cheap input selectors plus one result function, and it reruns the result function only when one of the input values changes — otherwise it hands back the previously cached result. createStructuredSelector composes a map of selectors into one selector that yields an object of their results, memoized the same way.
Build reselectSelectors, an object exposing createSelector and createStructuredSelector, plus a recomputations() counter on every selector so you can prove the cache is being reused.
type Selector = ((state: any, ...args: any[]) => any) & {
recomputations(): number;
};
const reselectSelectors: {
// (...inputSelectors, resultFn) -> a memoized selector
createSelector(...args: Function[]): Selector;
// { key: selector, ... } -> a selector returning { key: result, ... }
createStructuredSelector(map: Record<string, Function>): Selector;
};
const selectTotal = reselectSelectors.createSelector(
(state) => state.cart.items,
(items) => items.reduce((sum, item) => sum + item.price, 0),
);
const state = { cart: { items: [{ price: 10 }, { price: 5 }] } };
selectTotal(state); // 15 — runs the result function
selectTotal(state); // 15 — same items reference, returns the cached 15
selectTotal.recomputations(); // 1
const selectView = reselectSelectors.createStructuredSelector({
total: (state) => state.total,
user: (state) => state.user,
});
const r1 = selectView({ total: 42, user: { name: 'Ada' } });
// { total: 42, user: { name: 'Ada' } }
selectView.recomputations(); // 1
===. Immutable updates give a changed slice a new reference; an unchanged slice keeps its old one.recomputations() — returns how many times the result function ran, so a test can assert the cache was reused across calls.You will build createSelector, which wraps an expensive derivation so it only reruns when the exact pieces of state it reads change by reference, and createStructuredSelector, which composes several selectors into one object result.
A selector turns state into a derived value: a filtered list, a cart total, a formatted view model. Recomputing it is often expensive, and in a Redux app the same selector runs after every dispatched action, even actions that touched a completely unrelated slice of state. You want the selector to notice when its inputs are unchanged and hand back the exact same result it produced last time, so a downstream React.memo component or a useMemo can skip its work too. That is what reselect does: cheap input selectors pull out the slices you depend on, and one result function runs only when those slices actually change.
Think of a selector as a two-stage pipeline. The first stage is a set of cheap input selectors that pluck the slices you care about out of state. The second stage is one result function that turns those slices into the derived value, and it is the only expensive part. Between the two stages sits a gate: it compares this run's input values to the previous run's, slot by slot, with ===. If every slot matches, the gate short-circuits and returns the cached result without ever calling the result function.
The obvious version just runs the whole thing on every call:
function createSelector(...inputSelectors) {
const resultFn = inputSelectors[inputSelectors.length - 1];
const inputs = inputSelectors.slice(0, -1);
return function selector(state) {
const values = inputs.map((fn) => fn(state));
return resultFn(...values); // always recompute — no cache
};
}
It returns the right value, so it looks done. The trouble is it produces a brand-new result on every call. If the result function builds an array with items.map(...), each call returns a different array reference even when items never changed. Every downstream React.memo or useMemo that compares by reference now sees a new value and re-renders, which is the exact work a selector is meant to prevent.
The tempting over-correction is to cache the inputs and compare them with a deep equality check. That is correct, but it walks the entire input structure on every call, which is O(n) — for a large list you pay a full deep comparison every time, often more than the derivation you were trying to skip. Because Redux updates are immutable, a reference check gets the same answer for free: a real change always produces a new reference, and an unchanged slice always keeps its old one.
Keep the last inputs and last result in the closure. On each call, run the input selectors, compare their values to last time by reference, and rerun the result function only when something actually changed.
function createSelector(...args) {
// The last argument is the result function; the rest are input selectors.
const resultFn = args[args.length - 1];
const inputSelectors = args.slice(0, -1);
let lastInputs = null; // the input VALUES from the previous run, or null before the first run
let lastResult; // the cached output of resultFn
let recomputations = 0;
function selector(state, ...extraArgs) {
// Input selectors are cheap and always run; extraArgs lets a selector read a prop, e.g. an id.
const inputs = inputSelectors.map((input) => input(state, ...extraArgs));
// Compare this run's values to last run's, slot by slot, by reference.
const changed =
lastInputs === null ||
inputs.length !== lastInputs.length ||
inputs.some((value, i) => value !== lastInputs[i]);
if (changed) {
// A new reference in at least one slot: recompute and cache.
lastResult = resultFn(...inputs);
recomputations += 1;
lastInputs = inputs;
}
// Otherwise fall through and return the exact same result object as last time.
return lastResult;
}
selector.recomputations = () => recomputations;
return selector;
}
function createStructuredSelector(selectorMap) {
const keys = Object.keys(selectorMap);
const selectors = keys.map((key) => selectorMap[key]);
// Reuse createSelector: the members become the input selectors, and the
// result function stitches their values back into an object under the keys.
return createSelector(...selectors, (...values) => {
const result = {};
keys.forEach((key, i) => {
result[key] = values[i];
});
return result;
});
}
const reselectSelectors = { createSelector, createStructuredSelector };
module.exports = { reselectSelectors };
The whole cache is two closure variables: lastInputs (an array of the values the input selectors produced) and lastResult. The changed check treats the very first call, a different input count, or any slot that fails === as a change; everything else is a cache hit that returns lastResult untouched, so the result keeps its reference across calls. createStructuredSelector does no new memoization of its own: it hands the member selectors to createSelector as inputs and lets that one cache do the work, so the assembled object is rebuilt only when a member value changes.
Take a selector for the visible todos and trace four calls:
const selectVisible = reselectSelectors.createSelector(
(state) => state.todos,
(state) => state.filter,
(todos, filter) => todos.filter((t) => t.status === filter),
);
selectVisible(state) with state.filter of 'active'. lastInputs is null, so the gate reports a change. The result function runs, filters the list, caches the array, and recomputations becomes 1.state.todos and state.filter are the same references. inputs is [todos, 'active'], matching last run slot for slot, so the gate short-circuits: it returns the same filtered array and recomputations stays 1.todos array. Now state.todos is a new reference; slot 0 fails ===, the result function reruns, a fresh filtered array is cached, and recomputations becomes 2.recomputations stays 2.state.todos.push(todo) keeps the same array reference, so the selector never notices and serves a stale result. Fix: replace the array (state.todos = [...state.todos, todo]), the Redux rule.(s) => ({ ...s.user }) returns a fresh object every run, so the reference check always fails and you recompute every time, silently losing all memoization. Fix: input selectors should return existing references straight out of state.=== for a custom equalityFn such as a shallow-equal, useful when an input selector unavoidably returns a new array each run. This is what createSelectorCreator configures.n cache (an LRU keyed by inputs, or a WeakMap keyed by the arguments) fixes the alternating-input thrash, which modern reselect exposes through weakMapMemoize and a maxSize option.Keep practising the same patterns with a nearby challenge.
No submissions yet
Share your approach and start the discussion.