createStore + useSelectorLoading saved progress…

createStore + useSelector

A store with a selector lets each subscriber name the slice of state it cares about, so the store can wake it only when that slice changes. createGlobalState built the wire that lets React hear about a store at all. It held one value, and every subscriber wanted it. This question starts where that one stopped.

Once the store holds a whole app — a user, a cart, a draft — waking everyone is a performance bug you cannot see. A header reading state.user.name re-renders on every keystroke into state.draft, because the store rings every bell on every change. Nothing throws. Nothing looks broken. It is just slower than it looks, and this is why every real store ships a selector and an equality check.

Build createStore(initialState). The vanilla half is written for you; useSelector is the exercise.

Signature

function createStore<S>(initialState: S): {
  getState(): S;
  setState(patch: Partial<S> | ((prev: S) => Partial<S>)): void;
  subscribe(listener: () => void): () => void; // returns unsubscribe
  useSelector<T>(
    selector: (state: S) => T,
    isEqual?: (a: T, b: T) => boolean, // defaults to Object.is
  ): T;
};

Examples

Two siblings read two different slices of one store:

const store = createStore({ user: { name: 'ada' }, cart: { count: 0 } });

function Header() {
  const name = store.useSelector((s) => s.user.name);
  return <h1>{name}</h1>;
}

function CartBadge() {
  const count = store.useSelector((s) => s.cart.count);
  return <span>{count}</span>;
}

store.setState({ cart: { count: 3 } });
// CartBadge re-renders. Header does not — its answer did not move.

The second argument is for selectors that build their answer:

// filter() returns a new array every call, so this value is never Object.is
// equal to its own last answer — it re-renders forever.
store.useSelector((s) => s.items.filter((i) => i.done));

// Tell the store what unchanged means for this value, and it settles.
store.useSelector((s) => s.items.filter((i) => i.done), shallowEqual);

Notes

  • The vanilla half is given. getState, setState and subscribe are already written — they are the store from Observable Store and Mini Redux. Only useSelector is yours.
  • setState merges. setState({ a: 1 }) leaves the other keys untouched, and the updater form (prev) => partial computes the patch from the current state. It builds a new state object every time; it never mutates.
  • The default equality is Object.is. It is what React itself compares snapshots with. The second argument replaces it — see Object.is on MDN.
  • Inline selectors are the norm. useSelector((s) => s.a) is a brand-new function on every render. That must not resubscribe, churn, or loop.
  • You do not write shallowEqual. The tests pass one in.
  • Out of scope — memoised selectors (that is Reselect), reducers, actions, middleware, and persistence.

FAQ

Why does my component re-render when a part of the store it never reads changes?
Because a plain store notifies every subscriber on every write, and the subscriber has no way to say what it cared about. A selector plus an equality check fixes it: the store asks each subscriber its own question again and compares that answer with the one it last rendered, so a component reading state.user.name stays put when state.cart moves.
Why does useSelector crash with Maximum update depth exceeded?
Your selector builds its answer — filter, map, or an object literal — so it returns a new reference every call and is never Object.is-equal to its own last answer. React re-renders, calls it again, gets another new value, and loops. Pass a shallow equality function as the second argument, or memoise the selector so it returns a stable reference.
What is the default equality for useSelector?
Object.is, which is what React itself compares snapshots with. react-redux is the exception: it defaults to === instead, which differs from Object.is on NaN and on negative zero.
How is an equality function different from a memoised selector like reselect?
They answer different questions. An equality function decides whether a component re-renders by comparing two selected values; a memoised selector decides whether a value is recomputed by comparing its input slices. Both work by making the reference stable, which is why either one fixes the infinite loop, and you often want both.
Is it safe to write an inline arrow selector?
Yes. The selector is called during render, not stored, so a new function identity each render costs nothing and does not resubscribe. What matters is the value it returns, not the function that returned it.
Loading editor…