Skip to the content.

Selectors derive; they don't store

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.

Selectors sit between raw state and components, deriving views on read Raw normalised state on the left. Selectors in the middle derive total, visible list and count. Components on the right read only the derived values, never the raw shape. raw statenormalised selectTotal selectVisible selectCount components
State stays minimal and normalised; selectors are the seam that shapes it into what each component needs, on read.

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.