Context vs external stores
Context re-renders every reader when its value changes; an external store read with selectors re-renders only components whose selected value changed.
Predict first
Before you read: predict
ExamHall proctor console. Development build, no StrictMode. IDS holds 30 candidate ids.
One alert arrives.
How many times do CandidateTile and CandidateGrid run?
const ConsoleContext = createContext(null);function ConsoleProvider({ children }) { const [alerts, setAlerts] = useState([]); // the live feed calls setAlerts((l) => [...l, alert]) const value = { theme: 'dark', locale: 'en', alerts }; return <ConsoleContext value={value}>{children}</ConsoleContext>;}function AlertBadge() { const { alerts } = useContext(ConsoleContext); return <b>{alerts.length}</b>; }const CandidateTile = memo(function CandidateTile({ id }) { const { theme } = useContext(ConsoleContext); return <li className={theme}>{id}</li>;});function CandidateGrid({ ids }) { return <ul>{ids.map((id) => <CandidateTile key={id} id={id} />)}</ul>; }function App() { return <ConsoleProvider><AlertBadge /><CandidateGrid ids={IDS} /></ConsoleProvider>;}The 30-second version
A context hands one value to every component below its provider that reads it. When the value changes by Object.is, React re-renders each reader, memo or not, whatever field it uses; components passed in as children that don't read it stay. Split fast values off, or use an external store: a selector re-renders only when its result changes.
Context re-renders every reader when its value changes; an external store read with selectors re-renders only components whose selected value changed.
How it works
Step 1 of 10
useContext; the state lives in whichever component holds it.Analogy: A fire station's turnout alerting
| At the station | In React |
|---|---|
| Control sends a call and the bell rings in every room with a speaker | A new context value reaches every reader (step 2) |
| Everyone in those rooms gets up, even when the call is for another appliance | A reader re-renders whatever field it uses (step 3) |
| Earplugs issued against road noise do not stop the bell | memo does not shield a reader (step 4) |
| The kitchen has no speaker, so the cook carries on | Non-readers passed in as children stay (step 5) |
| One bell for the engine bay, another for the ladder bay | Split contexts (step 6) |
| Personal pagers that buzz when control changes the line for your own appliance | A store read with a selector (steps 7–8) |
| A pager that counts every reprint of the same line as a new call buzzes without end | An unstable snapshot loops (step 9) |
Breaks down when: a bell reaches every room at the same instant, but React renders readers one after another and, in a concurrent render, may pause between them. Wrong inference: "every component sees the same outside value, however it reads it." useSyncExternalStore makes React redo such an update, so every component shows the same store version (step 10).
Contrasting case: a crew briefed in person by its own officer, with no bell. That is a prop: it arrives because the parent rendered, and a parent's render reaches every crew it leads (react-render-commit step 2).
In real terms: React re-renders every component reading a provider whose value changed by Object.is; useSyncExternalStore re-renders a subscriber only when its getSnapshot result changed by Object.is.
Examples, run for real
4.1 ExamHall: the proctor console re-renders per alert (EXH-1490)
Live alerts reach the console several times a second. Header reads theme, AlertFeed reads the alerts, and StatusBoard renders 40 StatusRows that read no context. The first fix moves the state into a ConsoleProvider({ children }); MR !1690 then moves the alerts into an external store that AlertFeed reads with a selector.
4.1 EXH-1490: one context, provided inside ProctorConsole | one alert: Header 1, AlertFeed 1, StatusBoard 1, StatusRow 40
4.1 state moved into ConsoleProvider({ children }) | one alert: Header 1, AlertFeed 1, StatusBoard 0, StatusRow 0
4.1 MR !1690: + alerts in a store, read with a selector | one alert: Header 0, AlertFeed 1, StatusBoard 0, StatusRow 0
In the first row the 40 rows render because ProctorConsole itself renders them (react-render-commit step 2). In the second, only readers render, and Header renders for a theme that did not change (steps 3, 5). With the alerts in the store, the provider's prefs value never changes, so Header stays, and only AlertFeed's selected array is new (steps 7–8).
4.2 Proof domain, catalog: 24 price tags and a cart badge
Each PriceTag shows the shop's currency; CartBadge shows the cart count. The shopper adds one item.
4.2 ShopContext { currency, cartCount } | add to cart: CartBadge 1, PriceTag 24 | badge "1"
4.2 store, each reads its own field | add to cart: CartBadge 1, PriceTag 0 | badge "1"
The context re-renders all 24 tags for a currency that did not change (step 3). In the store, each tag's snapshot is its currency string, still "EUR" after the change, so the tags stay (step 8).
Picture it

Try it
Answer first, then check. Nothing is saved.
Question 1 of 5
Development build, no StrictMode. No component is wrapped in memo.
A buyer holds seat B7.
Apart from SeatingProvider, how many components re-render?
function SeatingProvider({ children }) { const [performance] = useState('Sat 19:30'); const [holds, setHolds] = useState({ held: [], expiresAt: '19:05' }); // holding a seat runs setHolds((x) => ({ ...x, held: [...x.held, seat] })) return ( <ShowContext value={performance}> <HoldsContext value={holds}>{children}</HoldsContext> </ShowContext> );}// App: <SeatingProvider><SeatMap seats={SEATS} /><Sidebar /></SeatingProvider>// SeatMap: 30 <SeatButton>, each reading ShowContext for its label// Sidebar: <HoldTimer /> reads holds.expiresAt · <HoldList /> reads holds.held// <BasketSummary /> reads ShowContext and holds.heldQuestion 2 of 5
Development build, no StrictMode. CONTACTS is a module-level array of 20 contacts.
A new message arrives.
Apart from ChatProvider, how many components re-render?
function ChatProvider({ children }) { const [unread, setUnread] = useState(0); // a new message runs setUnread((n) => n + 1) return <ChatContext value={{ me: 'u3', unread }}>{children}</ChatContext>;}function UnreadBadge() { const { unread } = useContext(ChatContext); return <b>{unread}</b>; }function Composer() { return <textarea />; }const ContactList = memo(function ContactList({ contacts }) { return <ul>{contacts.map((c) => <ContactRow key={c.id} contact={c} />)}</ul>;});function ContactRow({ contact }) { const { me } = useContext(ChatContext); return <li>{contact.id === me ? `${contact.name} (you)` : contact.name}</li>;}function App() { return <ChatProvider><UnreadBadge /><Composer /><ContactList contacts={CONTACTS} /></ChatProvider>;}Question 3 of 5
A catalog's deals page renders 48 DealRows inside startTransition, and React time-slices that render. Each row shows price * rates.eurUsd, reading rates straight from a module-level object that a WebSocket handler overwrites in place. The handler writes a new rate during one of React's breaks, and no state changes before the list commits.
What can the list show?
Question 4 of 5
Development build, no StrictMode. ledger is a small external store: setState replaces the state, then calls each listener.
PendingPanel crashes as soon as it mounts, before any payment arrives, with React's "Maximum update depth exceeded".
Which part causes it?
function useLedger(select) { return useSyncExternalStore( (onChange) => ledger.subscribe(onChange), () => select(ledger.getState()), );}function PendingPanel() { const pending = useLedger((s) => s.tx.filter((t) => t.status === 'pending')); const currency = useLedger((s) => s.currency); return <p>{pending.length} pending ({currency})</p>;}Question 5 of 5
Development build, no StrictMode. A live page:
Every viewer update re-renders all 30 rows.
Which single change stops a viewer update from re-rendering any ChannelRow, while a quality change still re-renders all 30?
function LiveProvider({ children }) { const [quality, setQuality] = useState('1080p'); // the viewer's quality menu const [viewerCount, setViewerCount] = useState(1200); // the feed bumps it several times a second return <LiveContext value={{ quality, viewerCount }}>{children}</LiveContext>;}function LivePage() { return <LiveProvider><ViewerBadge /><ChannelList /></LiveProvider>; }// ViewerBadge reads viewerCount; ChannelList reads nothing and renders 30 ChannelRow, each reading qualityGo deeper
Pro tips
- Before splitting a context, count who renders for one update. Put
const renders = useRef(0); renders.current++in each suspect reader, or record one update in the React DevTools Profiler, and compare the list with the components whose output changed.- When it helps: a context carries a value that changes several times a second (steps 2–3).
- Status: RAN (the counter), React 19.3.0 in jsdom. The Profiler UI was not exercised.
- Declare
subscribeoutside the component (or wrap it inuseCallback). react.dev: "If a different subscribe function is passed during a re-render, React will re-subscribe to the store using the newly passed subscribe function."- When it helps: a store hook written inline; it saves an unsubscribe and resubscribe per render (step 7).
- Verify: react.dev useSyncExternalStore, "My subscribe function gets called after every re-render".
- Status: DOC (fetched 2026-09-30); not executed here.
Source
- react.dev: useContext. Fetched 2026-09-30. "React automatically re-renders all the children that use a particular context starting from the provider that receives a different value. The previous and the next values are compared with the Object.is comparison. Skipping re-renders with memo does not prevent the children receiving fresh context values." Steps 2–4. Valid as of 2026-09-30.
- react.dev: memo. Fetched 2026-09-30. "Even when a component is memoized, it will still re-render when a context that it's using changes. Memoization only has to do with props that are passed to the component from its parent." Step 4. Valid as of 2026-09-30.
- react.dev: useSyncExternalStore. Fetched 2026-09-30. "While the store has not changed, repeated calls to getSnapshot must return the same value. If the store changes and the returned value is different (as compared by Object.is), React re-renders the component."; "if you always return a different value, you will enter an infinite loop"; and, for a store mutated during a Transition, React restarts the update "to ensure that every component on screen is reflecting the same version of the store." Steps 7–10, tip 2. Valid as of 2026-09-30.
- react.dev: Scaling Up with Reducer and Context. Fetched 2026-09-30. "The state still “lives” in the top-level TaskApp component … But its tasks and dispatch are now available to every component below in the tree by importing and using these contexts." Step 1; its provider components take
children(step 5). Valid as of 2026-09-30. - React 18 working group, "What is tearing?" (reactwg/react-18 #69, opened by Ricky Hanlon of the React team). Fetched 2026-09-30. "In a user interface, by “tearing” we mean that a UI has shown multiple values for the same state." Step 10 and P3. Valid as of 2026-09-30.
Related lessons
Checked against the tools and docs as of 2026-09-30.