Skip to content

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?

Code for this question
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>;}
Pick one

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

A channel, not a store. A provider hands one value to every descendant that calls 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

a charcoal cabinet with three drawers on a pale background. The middle drawer, lime-fronted with a lavender handle, is pulled out and holds four pale cyan folders; the top and bottom drawers are shut.
An illustration to help it stick. The mechanism is in the sections above.

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?

Code for this question
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.held
Pick one

Question 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?

Code for this question
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>;}
Pick one

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?

Pick one

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?

Code for this question
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>;}
Pick one

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?

Code for this question
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 quality
Pick one

Go deeper

Pro tips
  1. 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.
  2. Declare subscribe outside the component (or wrap it in useCallback). 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.

Checked against the tools and docs as of 2026-09-30.