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.

pressureselected direction
one temporary value coordinates a few descendantsuse ordinary React context.
a provider updates often and distinct consumers need independent fieldsprofile first, then use selector context.
a value comes from the server or persists across navigationkeep it in the query cache or service boundary.
a selector creates a new array or objectselect a stable field or memoize the derivation outside the selector.

good and bad

bad

// PanelContext stores ephemeral UI state: scroll offset, hover item, drag position
const PanelContext = createContext<PanelState>({
scrollOffset: 0,
hoveredItemId: null,
dragPosition: { x: 0, y: 0 },
items: [...],
});
// when user drags inside the panel, dragPosition updates 60 times per second
function PanelListItem({ itemId }: { itemId: string }) {
const panelState = useContext(PanelContext);
// component re-renders on every drag, even though this item doesn't move
const 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 offset
return <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 array
function PanelListItem({ itemId }: { itemId: string }) {
const items = useContextSelector(
PanelContext,
(state) => state.items,
);
// re-renders only when items array reference changes, not on drag/scroll
const isSelected = items.some(i => i.id === itemId && i.selected);
return <div>{isSelected && <span>✓</span>}</div>;
}
// consumer subscribes to only scroll offset
function PanelStatusBar() {
const scrollOffset = useContextSelector(
PanelContext,
(state) => state.scrollOffset,
);
// re-renders only when scroll offset changes
return <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.

search pages

go to any page