Selectors derive; they don't store
A selector is a function that takes the store’s state and returns a value derived
from it: selectCartTotal(state), selectVisibleTodos(state). The discipline it
enforces is simple and easy to violate — derive, don’t store. Anything you can
compute from existing state should be computed by a selector on read, not saved
as another field you have to keep in sync on write. The cart total is not a fact
to store; it is items.reduce(...). Store it and you have created a second source
of truth that will drift the first time someone updates an item without
recomputing the total.
The seam that decouples shape from components
The second reason selectors matter is refactoring. If a component reaches into
state.cart.items directly, then changing how the cart is stored — say,
normalising items into a byId map — means editing every component. Route every
read through a selector and the store’s shape is hidden behind a function you can
change in one place:
// the ONLY code that knows the raw shape lives here
export const selectItems = (state) => Object.values(state.cart.byId);
export const selectCartTotal = (state) =>
selectItems(state).reduce((sum, i) => sum + i.price * i.qty, 0);
// the component knows only the selector's name and its return value
const total = useSelector(selectCartTotal);
Reshape the store, fix the selectors, and every component keeps working.
Memoize the derivation so it isn’t recomputed
Deriving on every read is fine until the derivation is expensive or the component
re-renders often. That is what memoized selectors (reselect) are for: compose an
output selector from input selectors, and the result is recomputed only when an
input’s reference changes:
import { createSelector } from "reselect";
export const selectVisibleTodos = createSelector(
[selectItems, (state) => state.filter], // inputs
(items, filter) => items.filter((t) => t.status === filter) // recomputed only on change
);
If items and filter are unchanged references, selectVisibleTodos returns the
same array reference it returned last time — which means the component that
depends on it does not re-render. That reference stability is the performance
payoff, and it only works because reducers keep state immutable.
Keep state minimal, push logic into selectors
The healthiest stores are small: the irreducible facts, normalised, and nothing
that can be computed. Everything else — totals, filtered lists, counts, joins
across slices — lives in selectors. This keeps the write path simple (fewer fields
to update, fewer chances to desync) and puts the read-shaping where it belongs, in
composable functions you can test in isolation. The memoized-selector exercise
builds the createSelector chain above and is the fastest way to internalise why
a selector returning a fresh array every call quietly defeats the whole point.