use context selector
why
plain context broadcasts every change to every consumer: one high-frequency ephemeral value forces the whole subscriber tree to reconcile on each tick. selector subscriptions restore the invariant that a consumer re-renders only when the exact field it reads changes.
rule
use use-context-selector only for measured, high-churn ephemeral context
(pan position, scroll offset, input focus updating ~10x/s) where plain context
causes unrelated consumers to re-render, cascading into wasteful reconciliation
and blocking the event loop. create the context with the library, select the
smallest stable value each consumer needs, and keep server-synchronized facts
in their query cache.
the library is a subscription optimization, not a reason to place unrelated state in one provider or to turn context into persistent application storage.
preserve the owner boundary
split stable actions from changing state when they have different consumers. give each selector a primitive, an existing stable reference, or a memoized derived value. a selector that allocates a new object per call defeats selective subscription.
| pressure | selected direction |
|---|---|
| one temporary value coordinates a few descendants | use ordinary React context. |
| a provider updates often and distinct consumers need independent fields | profile first, then use selector context. |
| a value comes from the server or persists across navigation | keep it in the query cache or service boundary. |
| a selector creates a new array or object | select a stable field or memoize the derivation outside the selector. |
good and bad
bad
// PanelContext stores ephemeral UI state: scroll offset, hover item, drag positionconst PanelContext = createContext<PanelState>({scrollOffset: 0,hoveredItemId: null,dragPosition: { x: 0, y: 0 },items: [...],});// when user drags inside the panel, dragPosition updates 60 times per secondfunction PanelListItem({ itemId }: { itemId: string }) {const panelState = useContext(PanelContext);// component re-renders on every drag, even though this item doesn't moveconst isSelected = panelState.items.some(i => i.id === itemId && i.selected);return <div>{isSelected && <span>✓</span>}</div>;}function PanelStatusBar() {const panelState = useContext(PanelContext);// component re-renders on every scroll and drag, only needs scroll offsetreturn <div>Offset: {panelState.scrollOffset}</div>;}
dragging triggers 60 re-renders per second. PanelListItem computes .some()
on the whole items array 60 times per second even when the selection state
never changes. the browser becomes unresponsive.
good
const PanelContext = createContext<PanelState>({scrollOffset: 0,hoveredItemId: null,dragPosition: { x: 0, y: 0 },items: [...],});// consumer subscribes to only the items arrayfunction PanelListItem({ itemId }: { itemId: string }) {const items = useContextSelector(PanelContext,(state) => state.items,);// re-renders only when items array reference changes, not on drag/scrollconst isSelected = items.some(i => i.id === itemId && i.selected);return <div>{isSelected && <span>✓</span>}</div>;}// consumer subscribes to only scroll offsetfunction PanelStatusBar() {const scrollOffset = useContextSelector(PanelContext,(state) => state.scrollOffset,);// re-renders only when scroll offset changesreturn <div>Offset: {scrollOffset}</div>;}
each consumer subscribes to one stable field; consumers that do not use
dragPosition skip its 60 updates per second. responsiveness is preserved.
verification
profile the ordinary context first. after adoption, verify: the context was created by the library, each selector returns a referentially stable result for an unchanged input, and no consumer mixes the same changing fact through props and selector context.