“When the value in a context provider changes, what re-renders?” Candidates often answer “everything under the provider” or “nothing, because I used memo”. Both are wrong, and the precise answer explains why some apps slow to a crawl after someone puts the whole application state into one context.
Interviewers ask this to see whether you understand context as a dependency-injection mechanism with a real update cost, and whether you know how to scope it so that cost stays small.
Before you start
You should know how to create a context with createContext, provide it, and read it with useContext. React 19 also lets you render <ThemeContext value={...}> directly instead of <ThemeContext.Provider value={...}>, and read context with use(ThemeContext), including conditionally. Both forms behave the same for updates. Familiarity with memo and useMemo helps.
The short answer
When a provider renders with a value that differs from the previous one by Object.is, React re-renders every component below it that reads that context, even if those components or their parents are wrapped in memo. Components that do not read the context are unaffected by the context itself, though they may re-render for ordinary parent reasons. The most common performance bug is passing a new object literal as the value on every provider render. Fix it by memoising the value, splitting unrelated values into separate contexts, and placing providers as close as possible to the components that need them.
How it works
React tracks which fibers read which contexts. When a provider’s value changes, React walks its subtree and schedules a render for each consumer, skipping past any memo boundaries in between:
const ThemeContext = createContext('light');
const Toolbar = memo(function Toolbar() {
return <ThemedButton />; // memo cannot stop the button below
});
function ThemedButton() {
const theme = useContext(ThemeContext);
return <button className={theme}>Save</button>;
}If the theme changes, ThemedButton re-renders even though Toolbar is memoised and receives no props. That is intended: context exists precisely to reach components without threading props.
The problem appears with object values:
function App() {
const [user, setUser] = useState(null);
const [cartCount, setCartCount] = useState(0);
return (
<AppContext value={{ user, setUser, cartCount, setCartCount }}>
<Layout />
</AppContext>
);
}Every render of App creates a new object, so every consumer re-renders, including components that only read user when the cart count changes.
Step-by-step walkthrough
Step 1: Memoise the provider value
Wrap the value so its identity changes only when its contents do:
const value = useMemo(
() => ({ user, setUser, cartCount, setCartCount }),
[user, cartCount]
);
return <AppContext value={value}><Layout /></AppContext>;Now unrelated re-renders of App (say, a modal toggle) do not notify consumers. A cart change still re-renders the user consumers, because both live in one object.
Step 2: Split contexts by how often they change
Separate values with different update frequencies, and separate state from the functions that change it:
const UserContext = createContext(null);
const CartCountContext = createContext(0);
const CartActionsContext = createContext(null);
function Providers({ children }) {
const [user, setUser] = useState(null);
const [count, setCount] = useState(0);
const actions = useMemo(() => ({
add: () => setCount(c => c + 1),
clear: () => setCount(0),
}), []);
return (
<UserContext value={user}>
<CartActionsContext value={actions}>
<CartCountContext value={count}>{children}</CartCountContext>
</CartActionsContext>
</UserContext>
);
}An “Add to cart” button reads only CartActionsContext, whose value never changes, so it never re-renders when the count updates. Only the badge that reads CartCountContext does. The context splitting guide shows more variations.
Step 3: Scope providers to the subtree that needs them
A form wizard’s step state does not belong at the app root. Put its provider around the wizard. A lower provider means fewer components to walk and a smaller blast radius when the value changes. You can also nest providers of the same context to override values for a subtree, for example a dark-themed sidebar inside a light page.
Step 4: Use the children pattern so the provider’s owner re-renders less
Because Providers above receives children as a prop, a state change inside it re-renders the providers but not the children elements themselves, which were created by its parent. Only consumers update.
Worked scenario
A team keeps a global SettingsContext containing locale, theme, featureFlags and a live now timestamp updated every second for a clock in the header. After launch, typing into any form field feels sluggish.
The Profiler shows hundreds of components rendering every second. All of them call useContext(SettingsContext) to read locale or theme, and the value object changes every second because of now.
The fix has two parts. First, now is not a setting: it moves into the header’s clock component as local state. Second, the remaining values are memoised:
const settings = useMemo(
() => ({ locale, theme, featureFlags }),
[locale, theme, featureFlags]
);Per-second renders drop to one component. A further split of theme into its own context means toggling dark mode no longer re-renders every translated string.
Common mistake
- “
memostops context updates.” It does not; context reaches through memoised parents. A consumer itself cannot be blocked bymemoeither. - “Everything under the provider re-renders.” Only consumers re-render because of context. Other descendants re-render only if their parent did.
- Using context as a state manager for fast-changing data. Cursor positions, form keystrokes and timers in a broad context re-render many consumers. Keep that state local, or use an external store with selectors via useSyncExternalStore.
- Forgetting the default value is only used without a provider. It does not update and is not a fallback for
undefinedvalues.
Verify the behavior
Log renders in two consumers and change one value:
const CountContext = createContext(0);
const LabelContext = createContext('');
function ShowCount() { console.log('count consumer'); return <p>{useContext(CountContext)}</p>; }
function ShowLabel() { console.log('label consumer'); return <p>{useContext(LabelContext)}</p>; }
const Static = memo(() => { console.log('static'); return <><ShowCount /><ShowLabel /></>; });
export default function App() {
const [n, setN] = useState(0);
return (
<CountContext value={n}>
<LabelContext value="fixed">
<button onClick={() => setN(n + 1)}>+</button>
<Static />
</LabelContext>
</CountContext>
);
}Each click logs only count consumer. static and label consumer stay silent, proving context bypasses the memoised parent and touches only components that read the changed context. On React 18, write <CountContext.Provider value={n}> instead.
Follow-up questions
Can a component read only part of a context? No, there are no built-in selectors; a consumer re-renders on any change to the value. Split the context or use an external store with selectors.
What does use(Context) add in React 19? It reads context like useContext but can be called inside conditions and loops. Update behaviour is the same.
Is context slow? Context propagation itself is cheap. The cost is the consumers’ renders, which you control with scoping and value stability.
Where should the provider live in a Server Components app? Context only works in Client Components, so wrap a client provider component around server-rendered children. See Server Components boundaries.
Interview exercise
An AuthContext value is { user, login, logout }, created inline in the provider. A LogoutButton that calls useContext(AuthContext).logout re-renders whenever the user’s profile picture updates. Explain why and redesign it.
Answer and reasoning
The inline object is a new reference on every provider render, and any change to user produces a new value anyway. LogoutButton reads the context, so it re-renders on every change, even though logout never changes. Redesign by splitting: AuthStateContext holds user, and AuthActionsContext holds a useMemo-wrapped { login, logout } built from stable functions. LogoutButton reads only the actions context, whose value never changes, so profile updates do not touch it. Components that display the user still update as expected.
Continue learning
Practise with the React interview questions and the React MCQs. Related notes: context splitting for performance, memo and reference stability and external store subscriptions. Official reference: the useContext reference and Passing Data Deeply with Context on react.dev.