React Context Interview Questions
Short answer
Context passes a value down the tree without prop drilling. It is a delivery mechanism, not a state manager — it does not batch, memoise or select. Every consumer re-renders whenever the provider's value changes by reference, and almost every Context interview question comes back to that fact.
| Context | A state library (Redux, Zustand) | |
|---|---|---|
| Delivers a value deep in the tree | Yes | Yes |
| Owns the state | No — you still need useState/useReducer | Yes |
| Selective subscription | No — all consumers re-render | Yes, via selectors |
| Devtools and time travel | No | Yes |
| Middleware | No | Yes |
| Bundle cost | Zero — it is built in | A dependency |
| Good for | Theme, locale, auth, config | High-churn shared state |
What problem does Context solve?
Prop drilling. When a value is needed by a component five levels down, every component in between has to accept and forward a prop it does not use. Context lets a provider publish a value and any descendant read it directly with `useContext`, skipping the intermediate layers entirely. That is the whole feature — it is about delivery, not about ownership.
1const ThemeContext = createContext("light"); // default for orphans
2
3function App() {
4 const [theme, setTheme] = useState("dark");
5 return (
6 <ThemeContext.Provider value={theme}>
7 <Layout />
8 </ThemeContext.Provider>
9 );
10}
11
12function DeepButton() {
13 const theme = useContext(ThemeContext);
14 return <button className={theme}>Save</button>;
15 // → reads "dark" without any component in between passing it
16}The default value passed to `createContext` is used only when a consumer has no matching provider above it. That makes it useful for tests and Storybook, but it also means a typo in your provider placement fails silently with the default rather than throwing — which is why the custom-hook guard below is worth adopting as a habit.
Why does every consumer re-render when the value changes?
Because Context propagates by reference identity and has no way to compare parts of the value. When the provider's `value` prop is not identical to the previous render's, React re-renders every component that calls `useContext` for that context — regardless of whether the specific field they read actually changed. `React.memo` does not help, because the update reaches consumers through the context subscription rather than through props.
1function App() {
2 const [user, setUser] = useState(null);
3 const [theme, setTheme] = useState("dark");
4
5 // A NEW object on every render of App
6 return (
7 <AppContext.Provider value={{ user, setUser, theme, setTheme }}>
8 <Sidebar /> {/* reads only theme */}
9 <Profile /> {/* reads only user */}
10 </AppContext.Provider>
11 );
12}
13// → changing `theme` re-renders Profile too, and vice versa,
14// because the value object is new either wayThere are two separate problems tangled together in that snippet, and being able to pull them apart is the answer that scores. The first is that the object literal creates a new reference on every render, so consumers re-render even when nothing they care about changed. The second is that one context is carrying two unrelated concerns, so even a correctly memoised value would still notify everybody when either half moved.
A quick way to prove the re-render behaviour to yourself, and a good thing to describe in an interview, is to add a console log to each consumer and open React DevTools with "Highlight updates when components render" switched on. Change one field and watch every consumer flash. That two-minute experiment is what turns this from a rule you repeat into a behaviour you have actually observed, and interviewers can hear the difference.
How do you memoise the context value?
Wrap the value in `useMemo` with the state it depends on. That fixes the first problem: the provider can re-render for unrelated reasons without handing consumers a new reference. Setter functions from `useState` and `dispatch` from `useReducer` are already stable, so they do not need to be in the dependency array — though including them is harmless and keeps the linter quiet.
1function AuthProvider({ children }) {
2 const [user, setUser] = useState(null);
3
4 const value = useMemo(
5 () => ({ user, setUser }),
6 [user] // setUser is stable
7 );
8
9 return <AuthContext.Provider value={value}>{children}</AuthContext.Provider>;
10 // → the reference changes only when `user` changes, so an
11 // unrelated re-render of the parent no longer notifies consumers
12}A cheaper alternative that people forget: if the provider component does nothing except hold state and render `children`, the children passed to it are created by its parent and stay referentially stable across the provider's own re-renders. So a provider whose only job is state often does not re-render its subtree at all — the subtree is the same element object. That is worth mentioning because it explains why some providers appear not to have the problem.
Be careful with one common variation of this advice, though. Memoising the value fixes references but does nothing about the breadth of the notification — if the memo dependency changes, every consumer still re-renders. Candidates sometimes present `useMemo` as a complete answer to context performance and get caught by the follow-up. It solves the accidental updates; the split-context pattern below is what solves the unnecessary ones.
What is the split-context pattern?
Memoising fixes stale references but not the second problem: components that only dispatch still re-render when the state changes. The fix is to publish state and dispatch through two separate contexts. Because `dispatch` and `useState` setters never change identity, the dispatch context's value is constant — so components that only write never re-render when the state moves.
1const StateCtx = createContext(null);
2const DispatchCtx = createContext(null);
3
4function Provider({ children }) {
5 const [state, dispatch] = useReducer(reducer, initial);
6 return (
7 <StateCtx.Provider value={state}>
8 <DispatchCtx.Provider value={dispatch}>{children}</DispatchCtx.Provider>
9 </StateCtx.Provider>
10 );
11}
12
13function AddButton() {
14 const dispatch = useContext(DispatchCtx);
15 return <button onClick={() => dispatch({ type: "add" })}>Add</button>;
16 // → never re-renders when state changes; dispatch is stable forever
17}The same idea extends further: split by domain, not just by read and write. A theme context, an auth context and a cart context are three providers with three independent update frequencies, and a component that reads only the theme should never be woken by a cart change. Multiple small contexts are almost always better than one large one, and saying so preempts the performance follow-up.
This is 1 of 75+ questions in the React Interview Kit
Get every question with detailed answers, follow-ups and real code — plus coding challenges and a last-minute revision sheet. One-time payment, instant access.
⚡ Get the React Interview Kit → ₹399How do you write a safe custom hook for a context?
Export a hook rather than the context object. It gives you one place to throw a useful error when the provider is missing, it keeps the context private to its module, and it means consumers never import `useContext` and the context separately. This is the pattern nearly every library uses, and reaching for it unprompted reads as production experience.
1const AuthContext = createContext(undefined); // no default on purpose
2
3export function useAuth() {
4 const ctx = useContext(AuthContext);
5 if (ctx === undefined) {
6 throw new Error("useAuth must be used inside <AuthProvider>");
7 }
8 return ctx;
9}
10// → a missing provider fails loudly at the call site, instead of
11// silently handing back a default nobody notices for two weeksA small caveat on the custom-hook pattern: throwing is right for a context that is genuinely required, and wrong for one with a sensible default. An optional analytics context that quietly no-ops without a provider is easier to work with than one that crashes a page in a Storybook story. Decide which kind you are building, and make the default value say so.
When is Context the wrong tool?
Context is a poor fit for values that change many times a second, because every change notifies every consumer with no way to opt out. Form field state, mouse position, scroll offset, and anything animating should not live in context. It is also the wrong tool for server data — caching, refetching, deduping and invalidation are exactly what React Query and SWR provide and exactly what context does not.
- →Theme, locale, feature flags, current user — ideal for context; they change rarely.
- →Form state — keep it local, or use a form library; context re-renders on every keystroke.
- →Server data — React Query or SWR, which give you caching and invalidation for free.
- →High-churn global state — Zustand or Redux, which support selective subscription.
- →Two or three levels of prop drilling — just pass the prop; context is not free to read.
That last point is worth defending, because the instinct after learning context is to eliminate every repeated prop. Explicit props make a component's dependencies visible in its signature and its tests trivial to write; a context has to be found, mocked and provided. Reach for it when the value is genuinely global to a subtree and the drilling is genuinely painful — not at the first sign of a prop passed twice.
One more reason to prefer a custom hook: it gives you somewhere to add behaviour later without touching every call site. Logging, a development-only warning when the value is read outside an expected subtree, or a narrowed return type that hides internal fields all become one-line changes inside the hook. Exporting the raw context object forecloses all of that, because every consumer has already reached past you.
Can you use Context with Server Components?
Not in a Server Component itself. Context requires a runtime and a component tree that exists in the browser, so `createContext` and `useContext` are client-only — calling them in a Server Component is an error. The pattern is to create the provider in a file marked `"use client"`, then render it high in the tree and pass server-rendered content through it as children.
1// providers.tsx
2"use client";
3export function Providers({ children }) {
4 return <ThemeProvider>{children}</ThemeProvider>;
5}
6
7// app/layout.tsx — a Server Component
8export default function Layout({ children }) {
9 return (
10 <html><body>
11 <Providers>{children}</Providers>
12 </body></html>
13 );
14}
15// → the provider is a client boundary, but `children` is still
16// server-rendered and its code never reaches the browserHow do you test a component that uses Context?
Render it inside the real provider rather than mocking the hook. Mocking `useContext` couples the test to the implementation and stops catching genuine integration bugs; wrapping the component in the actual provider with a controlled initial state tests the thing you ship. A small custom render helper that applies your providers keeps this from becoming boilerplate in every file.
If the provider needs its own data, extract the state logic — usually a reducer — and test that as a pure function separately. That gives you fast, exhaustive coverage of the transitions and leaves the component tests to check that the right thing appears on screen. It is the same argument as testing a reducer outside React, and it is one reason `useReducer` pairs so naturally with context.
Does Context replace Redux?
For many apps, yes — context plus useReducer covers shared client state with no dependency. What you give up is selective subscription, devtools with time travel, and middleware. Redux Toolkit still earns its place in large applications; for server data, neither is the right answer and React Query is.
Can you nest providers of the same context?
Yes, and the nearest provider above a consumer wins. It is a legitimate pattern for scoped overrides — a dark-themed section inside a light page, for example — but it makes debugging harder because the value a component sees depends on where it sits in the tree.
Why does useContext not have a selector argument?
Because the context subscription notifies on the whole value and React has no way to know which part you used. A selector API has been discussed for years and use-context-selector exists as a userland library. In practice, splitting into more contexts is the supported answer.
What is the default value in createContext for?
It is used only when a consumer has no provider above it, which makes it handy for isolated tests and Storybook. Passing undefined deliberately, combined with a custom hook that throws, is usually better for application code because a missing provider then fails loudly.
Does React.memo prevent context re-renders?
No. React.memo only blocks re-renders caused by a parent re-rendering with equal props. A context update reaches the consumer through its subscription, bypassing props entirely, so a memoised component still re-renders when the context value changes.
Frequently asked questions
- What is React Context used for?
- Passing a value to deeply nested components without threading it through every intermediate component as a prop. It suits values that are genuinely global to a subtree and change rarely — theme, locale, the current user, feature flags — and it is a delivery mechanism rather than a state manager.
- Why does Context cause unnecessary re-renders?
- Because it propagates by reference identity with no partial comparison. When the provider's value prop is not identical to last render's, every component calling useContext for that context re-renders, whether or not the field it reads changed. Creating the value as an object literal in the provider makes this happen on every parent render.
- How do you optimise React Context performance?
- Memoise the value with useMemo so it only changes when the underlying state does, split state and dispatch into separate contexts so writers do not re-render on state changes, and split by domain so unrelated concerns do not share a provider. If you still need per-field subscription, that is the point to reach for a store library.
- What is the split context pattern?
- Publishing state through one context and dispatch through another. Because dispatch and useState setters keep the same identity forever, the dispatch context's value never changes, so components that only trigger updates never re-render when the state moves.
- Does React.memo stop context re-renders?
- No. React.memo compares props and only prevents re-renders that come from a parent rendering. Context updates arrive through the subscription rather than as props, so a memoised consumer re-renders anyway when the context value changes.
- Should I use Context instead of Redux?
- For shared client state in a small or medium app, context with useReducer is usually enough and adds no dependency. Redux Toolkit is worth it for large apps that want devtools, middleware and selective subscription. Server data belongs in React Query or SWR regardless of which you pick.
- Can Context be used in React Server Components?
- Not directly — createContext and useContext require a client runtime. Define the provider in a file marked "use client", render it high in the tree, and pass server-rendered content through as children. Those children stay on the server because they are rendered by the parent, not by the provider.
- What happens if there is no Provider above a useContext call?
- The consumer receives the default value passed to createContext, silently. That is why passing undefined as the default and exporting a custom hook that throws is the safer pattern — a missing provider then fails immediately at the call site instead of producing confusing behaviour later.
- How many contexts is too many?
- There is no hard limit, and more small contexts is usually better than one big one because each has its own update frequency. The practical ceiling is readability — if the provider stack in your root file is a dozen deep, group related ones into a single Providers component.
- Can you update a Context value from a child component?
- Yes, by including a setter or dispatch in the value. The state itself lives in the provider's useState or useReducer; the child calls the function it received through context and the provider re-renders with the new value, notifying every consumer.
- Is Context slow?
- Reading it is fast. The cost is the breadth of the update — every consumer re-renders on every value change, so the problem scales with how many components consume it and how often it changes. For rarely-changing values it is effectively free.
- How do you test components that use Context?
- Render them inside the real provider with a controlled initial state rather than mocking useContext, so the test exercises what you actually ship. Extract complex state logic into a reducer and unit-test that separately as a pure function.
This is 1 of 75+ questions in the React Interview Kit
Get every question with detailed answers, follow-ups and real code — plus coding challenges and a last-minute revision sheet. One-time payment, instant access.
⚡ Get the React Interview Kit → ₹399