Context selector pattern to stop re-rendering every consumer
Published December 15, 2026
import { createContext, useContext, useSyncExternalStore } from "react";
type State = { count: number; user: { name: string } };
type Listener = () => void;
const createStore = (initialState: State) => {
let state = initialState;
const listeners = new Set<Listener>();
const getState = () => state;
const setState = (patch: Partial<State>) => {
state = { ...state, ...patch };
listeners.forEach((listener) => listener());
};
const subscribe = (listener: Listener) => {
listeners.add(listener);
return () => listeners.delete(listener);
};
return { getState, setState, subscribe };
};
export const StoreContext = createContext(createStore({ count: 0, user: { name: "" } }));
export const useStoreSelector = <Slice,>(select: (state: State) => Slice): Slice => {
const store = useContext(StoreContext);
return useSyncExternalStore(store.subscribe, () => select(store.getState()));
};What
A "context selector" is a hook built on useSyncExternalStore that lets a component subscribe to just the slice of shared state it reads, so it only re-renders when that slice actually changes — not on every update to the store.
Why it matters
Plain useContext re-renders every consumer whenever the provided value changes, full stop — there's no way to say "I only care about user.name, not count." Splitting one big context into many small ones works, but only if your state naturally divides that way; a lot of app state doesn't. React doesn't ship a useContextSelector, and libraries like use-context-selector exist specifically to fill that gap using the same trick shown here: store the state outside React and subscribe to it with useSyncExternalStore, whose whole contract is "only re-render this component if the selected snapshot changed."
How it works
createStoreholdsstatein a closure, not in React state — mutations happen throughsetState, and every subscriber inlistenersis notified after each change.subscribeandgetStatematch the shapeuseSyncExternalStoreexpects: a way to register a change listener, and a way to read the current snapshot.StoreContextprovides a single store instance app-wide (noProviderneeded unless you want a scoped, per-subtree store instead).useStoreSelector(select)is the payoff: it callsuseSyncExternalStore(store.subscribe, () => select(store.getState())), so React only re-renders the calling component whenselect's return value actually changes between notifications.
Gotchas
selectmust return a value that's cheap to compute and ideally stable (Object.is-equal) when nothing relevant changed — returning a fresh object or array literal every call defeats the whole point, since every reference looks "different." This is also why the snippet keepslineWaiver: true: the store and the selector hook only prove the point together, and trimming either half leaves a non-functional example.- This pattern trades context's simplicity for manual store plumbing; reach for it only once you've confirmed re-renders from a shared context are an actual, measured problem.
setStatehere does a shallow merge ({ ...state, ...patch }) — nested objects still need to be replaced wholesale to be seen as changed by reference-based comparisons elsewhere in the app.
Related
useSyncExternalStore— the primitive this pattern is built on; see it applied to real browser state (not app state) in theuseSyncExternalStorepost on this blog.- Zustand or Redux with selectors — production libraries that implement this exact idea with far more ergonomics (equality functions, devtools, middleware) than this minimal version.