Context API vs Redux
Short answer
Context is not a state manager — it is a way to pass a value down the tree without prop drilling. Redux is a state container with a defined update mechanism, middleware and devtools. Context solves delivery; Redux solves ownership, transitions and debugging. Comparing them directly is the reason people get this wrong.
| Context API | Redux Toolkit | |
|---|---|---|
| What it is | A delivery mechanism | A state container |
| Avoids prop drilling | Yes | Yes |
| Holds state itself | No — you pair it with useState | Yes |
| Re-render control | None — all consumers re-render | Selectors, only what changed |
| Devtools | React DevTools only | Time-travel, action log, diffs |
| Middleware | No | Yes — logging, persistence, async |
| Bundle cost | Zero — it is in React | ~12 KB gzipped with RTK |
| Best for | Rarely changing app-wide values | Complex, frequently changing state |
Context is not a state manager
The whole confusion starts here. Context has no state of its own. It takes a value you already have and makes it available to any descendant without passing it through every layer between. The state still has to live somewhere — normally a useState or useReducer in the provider component — and context only carries it.
1const ThemeContext = createContext(null);
2
3function ThemeProvider({ children }) {
4 const [theme, setTheme] = useState("dark"); // ← the actual state
5 return (
6 <ThemeContext.Provider value={{ theme, setTheme }}>
7 {children}
8 </ThemeContext.Provider>
9 );
10}
11
12function Button() {
13 const { theme } = useContext(ThemeContext); // ← no props were drilled
14 return <button className={theme}>Buy</button>;
15}
16// → the comparison is really "useState + context" vs ReduxOnce you frame it that way, the real question stops being "which library" and becomes "does my state need what Redux adds" — a defined update mechanism, selector-based subscriptions, middleware and a devtools timeline. For a theme string, plainly not. For a collaborative editor's document model, plainly yes.
The re-render problem
This is the concrete, demonstrable reason context struggles as a store, and the part most explanations skip. When a context value changes, every component calling useContext for it re-renders — all of them, regardless of which part of the value they read. Context has no selector mechanism, and React.memo does not help because the update arrives through context rather than through props.
1const AppContext = createContext(null);
2
3function Provider({ children }) {
4 const [user, setUser] = useState(null);
5 const [cart, setCart] = useState([]);
6 const [theme, setTheme] = useState("dark");
7
8 return (
9 <AppContext.Provider value={{ user, setUser, cart, setCart, theme, setTheme }}>
10 {children}
11 </AppContext.Provider>
12 );
13}
14
15function ThemeToggle() {
16 const { theme, setTheme } = useContext(AppContext);
17 // → re-renders when the CART changes. It does not read the cart.
18}
19// Adding one item to the cart re-renders every consumer in the app.There is a second, subtler version that catches people who have done everything else right. The object literal passed as `value` is recreated on every render of the provider, so even when none of the underlying state changed, a re-render of the provider's parent gives every consumer a brand-new value and re-renders them all.
1// New object every render → every consumer re-renders every time
2<AppContext.Provider value={{ user, setUser }}>
3
4// Stable identity → consumers re-render only when user actually changes
5const value = useMemo(() => ({ user, setUser }), [user]);
6<AppContext.Provider value={value}>
7
8// Even better: split by change frequency
9<UserContext.Provider value={user}>
10 <SetUserContext.Provider value={setUser}>
11// → setUser never changes, so components that only dispatch never re-renderMaking context perform: the split-context pattern
You can get a long way with context by separating the value from the way you change it. Most components either read state or dispatch changes, rarely both. Two contexts — one for the data, one for the dispatch function — means the dispatch-only components never re-render, because a dispatch function's identity is stable for the life of the component.
1const StateContext = createContext(null);
2const DispatchContext = createContext(null);
3
4function CartProvider({ children }) {
5 const [state, dispatch] = useReducer(cartReducer, initial);
6 // dispatch is referentially stable — React guarantees it
7 return (
8 <StateContext.Provider value={state}>
9 <DispatchContext.Provider value={dispatch}>
10 {children}
11 </DispatchContext.Provider>
12 </StateContext.Provider>
13 );
14}
15
16function AddButton({ sku }) {
17 const dispatch = useContext(DispatchContext);
18 return <button onClick={() => dispatch({ type: "add", sku })}>Add</button>;
19 // → never re-renders when the cart changes. It only writes.
20}That pattern — useReducer for the transitions, split contexts for delivery — is genuinely close to Redux in shape, with no dependency and no boilerplate beyond the reducer. If you can describe it in an interview you have effectively answered "could you build a small Redux?", which is a question that follows this one surprisingly often.
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 → ₹399What Redux adds that context cannot
Three things, and being able to name them precisely is the difference between an opinion and an argument.
First, selective subscriptions. `useSelector` runs your selector after every dispatch and re-renders the component only if the selected slice actually changed, so a component reading `state.cart.total` ignores every user or theme update. Context has no equivalent, and this is the gap that matters most as an app grows.
1function CartBadge() {
2 const count = useSelector(s => s.cart.lines.length);
3 // → re-renders only when the number of lines changes.
4 // Theme changes, user changes and price edits are all ignored.
5 return <span>{count}</span>;
6}
7
8// The context equivalent re-renders on every context change,
9// then React bails out only if the rendered OUTPUT is identical —
10// which still cost you the render.Second, middleware. Every action passes through a pipeline you control, which is how logging, analytics, persistence to localStorage, crash reporting and optimistic sync get implemented once rather than in fifty components. There is no interception point in context at all.
Third, the devtools. Every action is named, every state change is diffed, and you can step backwards through the sequence that produced a bug. On a complex flow — a checkout that computed the wrong total, a wizard that skipped a step — this turns a two-hour investigation into a two-minute one. It is the single most underrated reason to keep Redux.
So when do you actually need Redux?
There is a usable test. If your state changes frequently, is read by many unrelated components, and moves through transitions complex enough to deserve names, a store earns its place. If any one of those is false, context or plain state is probably enough — and if all three are false, you are adding a dependency to avoid passing two props.
| State | Changes | Readers | Use |
|---|---|---|---|
| Theme, locale | Rarely | Many | Context |
| Logged-in user | Rarely | Many | Context |
| Form draft | Often | One subtree | useState, lifted |
| Shopping cart | Often | Many | Store, or reducer + split context |
| Editor undo stack | Often | Many | Redux — devtools pay for themselves |
| Fetched API data | Often | Many | React Query, not either |
| Modal open/closed | Often | One or two | useState |
The lighter alternatives
The choice is no longer binary. Several small libraries give you selective subscriptions — the feature context lacks — without Redux's ceremony, and they are a reasonable default for new projects that need a store but not a full architecture.
| Option | Size | Strength | Weakness |
|---|---|---|---|
| Context + useReducer | 0 KB | No dependency, familiar | No selectors |
| Zustand | ~1 KB | Selectors, no provider | Thinner devtools |
| Jotai | ~3 KB | Atomic, fine-grained | Different mental model |
| Redux Toolkit | ~12 KB | Devtools, middleware, ecosystem | Most ceremony |
| React Query | ~13 KB | Server state done properly | Not for client state |
1const useCart = create((set) => ({
2 lines: [],
3 add: (line) => set((s) => ({ lines: [...s.lines, line] })),
4 clear: () => set({ lines: [] }),
5}));
6
7function Badge() {
8 const count = useCart((s) => s.lines.length);
9 // → selector-based, so this ignores unrelated changes.
10 // No provider, no slice, no dispatch.
11 return <span>{count}</span>;
12}Context in Next.js and Server Components
One practical wrinkle worth knowing, because it comes up in every Next.js App Router project. Context is a client-side feature: a provider needs `"use client"`, and Server Components cannot consume context at all. Wrapping your whole app in a provider therefore pulls the entire tree into the client bundle, which defeats the point of Server Components.
1// app/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 <body><Providers>{children}</Providers></body>;
10}
11// → children passed through stay Server Components; only components
12// that actually call useContext become client components.The rule that follows: pass Server Components as `children` through a client provider rather than importing them inside it. The provider renders on the client, the children were already rendered on the server, and you keep both.
Is context slow?
Context itself is fast — the propagation cost is negligible. What is slow is re-rendering every consumer when only one of them cared about the change. Split contexts by change frequency, memoise the value, and the problem largely disappears for values that change occasionally.
Can I use multiple contexts?
Yes, and you should. Multiple narrow contexts perform better than one wide one because each consumer subscribes only to what it needs. The usual split is one per concern — theme, auth, locale — plus the state/dispatch split within a concern that changes often.
Does React.memo prevent context re-renders?
No. React.memo compares props, and a context update does not arrive through props. A memoised component that calls useContext still re-renders when that context's value changes. The only ways out are narrower contexts or a store with selectors.
What is useContextSelector?
A proposed API that would let a component subscribe to part of a context value, giving context the selector behaviour it currently lacks. It is not in React yet; the use-context-selector package implements it today, and most teams reach for a small store instead.
Do I need Redux for a medium-sized app?
Usually not, if server data is handled by a query library. What is left is generally a handful of client concerns that context or a small store covers. Add Redux when you want the devtools and middleware, or when the state transitions are complex enough that named actions genuinely help.
Frequently asked questions
- What is the difference between Context API and Redux?
- Context is a mechanism for passing a value down the component tree without prop drilling — it holds no state itself. Redux is a state container with a defined update mechanism, selector-based subscriptions, middleware and devtools. Context solves delivery; Redux solves managing and debugging state.
- Can Context API replace Redux?
- For app-wide values that change rarely — theme, locale, the current user — yes, comfortably. For frequently changing state read by many components it struggles, because every consumer re-renders on every change and there is no way to subscribe to part of the value.
- Why use Redux instead of Context API?
- Three reasons that context cannot match: selectors, so a component re-renders only when the slice it reads changes; middleware, for logging, persistence and analytics in one place; and time-travel devtools, which turn a hard state bug into a short investigation.
- Is Context API bad for performance?
- Not inherently. The problem is that a context update re-renders every consumer regardless of which part of the value they use. Memoise the provider value with useMemo, split large contexts into smaller ones, and separate state from dispatch — that covers most of the cost.
- Why do all my components re-render when I use context?
- Either the value changed and every consumer is subscribed to the whole thing, or — more often — you are passing a fresh object literal as value, so its identity changes on every provider render even when the underlying state did not. Wrap it in useMemo.
- Does React.memo stop context re-renders?
- No. React.memo only compares props, and context updates do not arrive as props. A memoised component that calls useContext will still re-render whenever that context's value changes. Narrower contexts or a store with selectors are the real fixes.
- When should I use useReducer with Context?
- When several related values change together and the transitions are worth naming — a cart, a multi-step form, a filter panel. Put the reducer's state in one context and its dispatch in another, so components that only dispatch never re-render.
- Is Redux still needed in 2026?
- Less often than it used to be. Server data belongs in a query library, prop drilling is handled by context, and lighter stores cover most client state. Redux still wins where you want middleware and time-travel debugging, or where a large team benefits from explicit named actions.
- What is the difference between Context API and Zustand?
- Zustand is an actual store with selector-based subscriptions, so components re-render only when the part they read changes — the feature context lacks. It also needs no provider. Context is built into React with zero bundle cost; Zustand is about 1KB and better suited to frequently changing state.
- Can Server Components use Context?
- No. Context is a client-only feature, so a provider must be marked "use client" and Server Components cannot call useContext. Keep providers as low in the tree as possible and pass Server Components through as children so they are not pulled into the client bundle.
- How many contexts is too many?
- There is no hard limit, and more narrow contexts usually perform better than fewer wide ones. The practical ceiling is readability: once the provider nesting is hard to follow, wrap them in a single Providers component, or move the frequently changing pieces into a store.
- Should I use Context or Redux for authentication state?
- Context, in most cases. The current user changes rarely — at login, logout and token refresh — which is exactly the profile context handles well. The token itself should live in an HttpOnly cookie rather than in either, with only the user's display data in the context.
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