Lesson 57 of 80
Context re-renders every consumer on any value change. The selector pattern lets a consumer subscribe to just the slice it actually reads.
15 min read
Every option explains itself — why the right one is right, and why each wrong one is wrong. Miss one and it comes back later on a spaced-repetition schedule.
This topic is about judgment and practical tradeoffs more than a single right answer — a mock interview is the better way to test yourself on it.
Be interviewed on this →Hints without spoilers, and a plain explanation of why a wrong answer was wrong — answered only from this lesson. Free account, no card.
Sign up freeWhat interviewers actually ask about this, and what a strong answer has to cover.
A strong answer covers
Likely follow-ups
The context lesson already covers the core limitation: a Provider's consumers all re-render when the provided value changes, even if a given consumer only reads one field of it. Splitting into several narrower contexts helps, but sometimes you have one genuinely related bundle of state (a big form, a canvas editor's selection state) where splitting it into ten separate contexts is its own kind of mess.
The selector pattern solves it differently: instead of subscribing to the whole value, a consumer passes a selector function and only re-renders when the piece that function returns actually changes. React's built-in Context doesn't support this natively — but you can build it on top of useSyncExternalStore, which is exactly what libraries like use-context-selector and zustand's context bindings do under the hood.
function createSelectorContext() {
const StoreContext = createContext(null);
function Provider({ value, children }) {
const listeners = useRef(new Set()).current;
const valueRef = useRef(value);
valueRef.current = value;
// Notify subscribers after every render where `value` changed identity.
useEffect(() => {
listeners.forEach((listener) => listener());
});
const store = useRef({
getSnapshot: () => valueRef.current,
subscribe: (listener) => {
listeners.add(listener);
return () => listeners.delete(listener);
},
}).current;
return <StoreContext.Provider value={store}>{children}</StoreContext.Provider>;
}
function useSelector(selector) {
const store = useContext(StoreContext);
return useSyncExternalStore(store.subscribe, () => selector(store.getSnapshot()));
}
return { Provider, useSelector };
}
const UserContext = createSelectorContext();
function App() {
const [user, setUser] = useState({ name: "Adesh", theme: "dark" });
return (
<UserContext.Provider value={user}>
<NameBadge />
<ThemeToggle onToggle={() => setUser((u) => ({ ...u, theme: u.theme === "dark" ? "light" : "dark" }))} />
</UserContext.Provider>
);
}
function NameBadge() {
const name = UserContext.useSelector((u) => u.name);
return <span>{name}</span>; // never re-renders when only `theme` changes
}
The trick is useSyncExternalStore: on every notification it calls getSnapshot again, but it only triggers a re-render if the returned value differs from last time (Object.is comparison). Since getSnapshot here is selector(store.getSnapshot()), NameBadge only re-renders when u.name itself changes — a theme update produces a new name snapshot that's Object.is-equal to the old one, so React bails out.
(u) => ({ name: u.name }), it returns a new object every call and Object.is never matches — you've silently defeated the whole pattern. Select primitives or already-stable references.useSyncExternalStore's snapshot function must be cheap and pure. It can run on every render just to check for changes, not only on actual updates — an expensive selector here is a real performance cost, not a free lookup.This is a direct answer to the re-render problem the context lesson describes — instead of avoiding it by re-architecting into multiple providers, it avoids it by changing what "subscribing" means for a single provider. Underneath, it's the same external-store subscription model React itself uses to read the browser's own state (window size, online status) safely.