The question every React interviewer asks
"When would you use Context instead of Redux?" is one of those interview questions that reveals more about a candidate than almost any other. A junior answer is "Context is simpler." A mid-level answer names some trade-offs. A senior answer starts with re-render behaviour, works through middleware and selector granularity, and ends with a concrete decision framework.
This guide gives you the full picture — not just the surface-level talking points, but the mechanics that explain why the trade-offs exist. By the end you'll be able to answer the question from first principles, which is exactly what experienced interviewers are listening for.
What Context API Actually Is (and Isn't)
The Context API is a dependency injection mechanism, not a state manager. That distinction matters.
When you write createContext and wrap a subtree in a Provider, you're
creating a channel through which a value can be read by any descendant without
threading it through props. You still manage the state yourself — with
useState, useReducer, or anything else — and Context simply makes it
available.
const ThemeContext = createContext('light') // 'light' is the default (no Provider)
function App() {
const [theme, setTheme] = useState('dark')
return (
<ThemeContext.Provider value={theme}>
<Layout />
</ThemeContext.Provider>
)
}
function Button() {
const theme = useContext(ThemeContext) // 'dark' — no props needed
return <button className={`btn btn--${theme}`}>Click</button>
}
This is genuinely elegant for ambient data that the whole app needs but that changes rarely: the authenticated user object, the active locale, the colour theme, feature flags loaded at startup. These values are read in dozens of places but typically update only once or twice per session — at login, on a theme toggle, on a language switch.
The mental model: Context is a conduit. The state still lives in a
useState call somewhere; Context just makes it accessible at a distance.
The Re-render Problem — Why Context Doesn't Scale for Frequent Updates
Here is the behaviour that trips up most candidates when they first encounter it.
Every component that calls useContext(MyContext) re-renders when the
Provider's value prop changes — and "changes" means the reference is
different, not just the content. React does not diff the previous and next
context values at a field level. It compares references.
function AppProvider({ children }) {
const [user, setUser] = useState(null)
const [cart, setCart] = useState([])
// ❌ A new object is created on every render of AppProvider.
// Adding one item to the cart re-renders EVERY component that
// calls useContext(AppContext), including UserBadge, HeaderNav,
// SettingsPanel — anything that subscribes to this context.
return (
<AppContext.Provider value={{ user, setUser, cart, setCart }}>
{children}
</AppContext.Provider>
)
}
You can partially address this with useMemo:
const value = useMemo(
() => ({ user, setUser, cart, setCart }),
[user, cart] // setUser / setCart are stable references from useState
)
return <AppContext.Provider value={value}>{children}</AppContext.Provider>
Now value only gets a new reference when user or cart actually changes.
But here's the crucial limitation: every subscriber still re-renders whenever
user or cart changes, even components that only need user and have no
business re-rendering because cart changed.
Redux solves this with selectors:
// With Redux — only re-renders when state.auth.user changes
const user = useSelector(state => state.auth.user)
// Adding an item to the cart dispatches a different slice;
// this component does not re-render at all
useSelector compares the selector's return value between renders (using
strict equality by default). If the result hasn't changed, the component is
skipped. This is subscription granularity — and Context has no native
equivalent.
In practice this means: use Context for values that update infrequently, split your contexts so each has one reason to change, and switch to Redux (or Zustand) once updates become frequent or consumers multiply.
What Redux Adds — Middleware, DevTools, Selectors
Redux's value over Context comes from three capabilities that Context simply cannot replicate.
Middleware
The Redux middleware pipeline sits between dispatch and the reducer. Every
action flows through it, giving you a place to intercept, transform, delay, or
fork into side effects.
redux-thunk (built into Redux Toolkit) allows action creators to be async
functions:
// Without thunk you can only dispatch plain objects.
// With thunk you dispatch a function that receives dispatch and getState.
export const fetchUser = createAsyncThunk('users/fetch', async (id) => {
const response = await api.getUser(id)
return response.data
})
// RTK's createAsyncThunk automatically dispatches:
// users/fetch/pending → set loading: true
// users/fetch/fulfilled → set data, loading: false
// users/fetch/rejected → set error, loading: false
redux-saga goes further — generators let you model complex async flows like
"take the latest search request, cancel the previous one, retry on failure."
None of this is available in Context. You can write async functions inside a
Context Provider, but you lose the action log, DevTools visibility, and the
ability to compose or cancel flows at the middleware layer.
DevTools
Redux DevTools is genuinely transformative for debugging. You get a full, ordered log of every dispatched action, a diff of what changed in the store for each one, and time-travel: click any past action to jump the UI back to that exact state.
Redux Toolkit wires this up automatically in development mode — no configuration needed. Context has no equivalent. The closest you can get is React DevTools component inspection, which shows you current state but gives you no history, no diffs, and no replay.
Selectors
useSelector from react-redux lets you derive and subscribe to a specific
slice of the store. Reselect's createSelector adds memoisation — the derived
value is only recomputed when its inputs change:
const selectVisibleTodos = createSelector(
[state => state.todos, state => state.filter],
(todos, filter) => todos.filter(t => matchesFilter(t, filter))
)
// Component re-renders only when the filtered result actually changes
const visibleTodos = useSelector(selectVisibleTodos)
This kind of computed, memoised, fine-grained subscription is not available in Context without third-party libraries.
Context + useReducer — The Lightweight Middle Ground
Before reaching for Redux, there is an intermediate option that works well for
small-to-medium apps: pairing useReducer with Context.
const CounterStateContext = createContext(null)
const CounterDispatchContext = createContext(null) // split so dispatch-only consumers don't re-render
function counterReducer(state, action) {
switch (action.type) {
case 'increment': return { count: state.count + 1 }
case 'decrement': return { count: state.count - 1 }
case 'reset': return { count: 0 }
default: throw new Error(`Unknown: ${action.type}`)
}
}
export function CounterProvider({ children }) {
const [state, dispatch] = useReducer(counterReducer, { count: 0 })
return (
<CounterStateContext.Provider value={state}>
<CounterDispatchContext.Provider value={dispatch}>
{children}
</CounterDispatchContext.Provider>
</CounterStateContext.Provider>
)
}
The key insight: dispatch is a stable reference (like useState's setter),
so putting it in its own context means components that only dispatch will never
re-render due to state changes.
This pattern gives you the same conceptual separation Redux offers — actions, reducer, centralised state — without the dependency. What it still lacks: middleware, DevTools, and selector granularity. Once your app needs any of those three things, the incremental complexity of RTK is justified.
A good rule of thumb: Context + useReducer scales comfortably up to about five independent pieces of complex state. Beyond that, the manual wiring required matches or exceeds Redux Toolkit's setup cost.
Decision Framework — When to Pick Each
The right tool depends on three axes: how often the state updates, how complex the async logic is, and how large the team is.
Use Context when:
- The state changes infrequently — theme, auth user, locale, feature flags.
- No middleware is needed; async logic is a single
async/awaitcall. - The app is small or the state is scoped to a subtree.
- You want zero external dependencies.
Use Redux Toolkit when:
- State updates frequently (cart updates, real-time feeds, form state with many interdependent fields).
- You need async coordination: loading states, cancellation, polling, retries.
- Different parts of the store need to derive values from each other (selectors that combine slices).
- Multiple engineers work on the codebase and enforced conventions reduce accidental coupling.
- Time-travel debugging or an action audit trail has real value.
Use Zustand or Jotai when:
- You need granular subscriptions but Redux feels like overkill.
- You want to avoid the Provider wrapping ceremony.
- Zustand: a single
createcall gives you a store with selectors, no Provider needed. - Jotai: atomic model — each atom is independent, components subscribe to only the atoms they read.
One thing to get right: server state is not the same as client state. Loading indicators, cached API responses, and background re-fetching are not a good fit for either Context or Redux. React Query, SWR, or RTK Query are purpose-built for this; layering server state into a Redux store adds complexity with no gain.
What a Great Interview Answer Looks Like
When an interviewer asks "Context vs Redux?", they are not looking for a feature comparison. They are looking for evidence that you understand the mechanics — re-render semantics, selector granularity, middleware — and that you can reason about trade-offs rather than recite a preference.
A strong answer has four parts:
- Clarify what Context is — a transport mechanism, not a state manager. It shares a value; it does not define how that value is updated or structured.
- Name the re-render problem — any change to the context value re-renders all consumers. There is no selector layer to limit this. For infrequently updated values this is fine; for frequent updates it becomes a bottleneck.
- Explain what Redux adds — middleware for async, DevTools for debugging,
useSelectorfor granular subscriptions. These are the specific gaps Context cannot fill without third-party libraries. - Give a concrete decision heuristic — start with local state, promote to Context for ambient global data (theme, auth, locale), reach for Redux when your state has behaviour that Context can't model: async sequences, derived computed values, audit trails. Mention Zustand/Jotai as a pragmatic middle ground.
Interviewers who ask this question have usually been burned by a Context anti-pattern in production — probably a large monolithic context that triggered cascading re-renders across the whole tree. Show them you understand why that happens and how you'd prevent it, and you'll stand out from candidates who only know the surface-level talking points.