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.
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;
};
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);
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.Object.is. It is what React itself compares snapshots with. The second argument replaces it — see Object.is on MDN.useSelector((s) => s.a) is a brand-new function on every render. That must not resubscribe, churn, or loop.shallowEqual. The tests pass one in.