Context API vs Redux: When to Use Each
Advertisement
🧩 Scenario
Architecture Walkthrough
What Context Provides
React's Context API solves one problem: sharing values across a component tree without passing props through every intermediate component. You create a context with createContext, wrap a subtree in a Provider with a value, and any descendant reads the value with useContext. The component does not need to know about the depth of the tree or the components between it and the provider.
Context is not a state manager. It is a dependency injection mechanism. The state itself is managed by useState or useReducer in the Provider component; Context just makes that state accessible to any descendant without prop drilling.
The Re-Render Gotcha
Every component that calls useContext(MyContext) subscribes to the entire context value. When the context value object changes (even if only one field in it changed), all subscribers re-render. This is a significant difference from Redux, where selectors allow components to subscribe to specific slices of state.
The mitigation is to split contexts by update frequency. State that rarely changes (user session, theme) goes in one context. State that updates frequently (cart, notifications) goes in another. Components that only read the user session are not affected by cart updates.
What Redux Toolkit Adds
Redux Toolkit adds the action-reducer model: state changes are described as dispatched actions, and reducers are pure functions that produce new state from an action. This enforces a predictable, auditable state machine. Middleware (Redux Thunk or Redux Saga) handles async actions, including loading and error states, as part of the action lifecycle. The Redux DevTools extension provides time-travel debugging, action history, and state diffs. Selectors with createSelector compute derived state and memoize it, preventing unnecessary re-renders.
Context with useState provides none of these. It is the right tool for simple, infrequently updated global state.
Key Code Explained
// Context: create, provide, consume
interface UserContextValue {
user: User | null;
signOut: () => void;
}
const UserContext = createContext<UserContextValue | null>(null);
function useUser() {
const ctx = useContext(UserContext);
if (!ctx) throw new Error('useUser must be inside UserProvider');
return ctx;
}
function UserProvider({ children }: { children: React.ReactNode }) {
const [user, setUser] = useState<User | null>(null);
const signOut = useCallback(() => {
setUser(null);
clearAuthToken();
}, []);
// The value object must be stable — useMemo prevents recreation on every render
const value = useMemo(() => ({ user, signOut }), [user, signOut]);
return <UserContext.Provider value={value}>{children}</UserContext.Provider>;
}
// Consumer: reads user context
function NavBar() {
const { user, signOut } = useUser();
return (
<nav>
<span>{user?.name}</span>
<button onClick={signOut}>Sign Out</button>
</nav>
);
}
// Split contexts by update frequency to avoid cross-concern re-renders
function AppProviders({ children }: { children: React.ReactNode }) {
return (
// UserContext: session changes rarely
<UserProvider>
{/* CartContext: updates on every add/remove/quantity change */}
<CartProvider>
{/* NotificationContext: updates on every new notification */}
<NotificationProvider>
{children}
</NotificationProvider>
</CartProvider>
</UserProvider>
);
}
// NavBar reads UserContext → only re-renders on session changes
// CartIcon reads CartContext → only re-renders on cart changes
// Redux Toolkit comparison: slice-based state
import { createSlice, configureStore } from '@reduxjs/toolkit';
const cartSlice = createSlice({
name: 'cart',
initialState: { items: [] as CartItem[], total: 0 },
reducers: {
addItem(state, action: PayloadAction<CartItem>) {
state.items.push(action.payload); // Immer makes this safe
state.total += action.payload.price;
},
removeItem(state, action: PayloadAction<string>) {
const index = state.items.findIndex((i) => i.id === action.payload);
if (index >= 0) {
state.total -= state.items[index].price;
state.items.splice(index, 1);
}
},
},
});
// Selector: only re-renders consumer when count changes, not on price changes
const selectCartCount = (state: RootState) => state.cart.items.length;
function CartIcon() {
const count = useSelector(selectCartCount); // fine-grained subscription
return <span>{count} items</span>;
}
The useMemo on the context value in UserProvider is important. Without it, the value object is recreated on every render of UserProvider, even when user and signOut have not changed. Because context comparison uses reference equality, a new object reference triggers re-renders in all consumers even when the actual data is identical.
Tradeoffs
| Feature | Context + useState | Redux Toolkit | Zustand |
|---|---|---|---|
| Setup complexity | None | Moderate | Low |
| Fine-grained subscriptions | No (must split) | Yes (selectors) | Yes (selectors) |
| Middleware / async actions | No built-in | Yes (Thunk) | Yes (simple) |
| DevTools / time-travel | No | Yes | Basic |
| Bundle size | Zero | ~10KB gzipped | ~1KB gzipped |
| Best for | Simple, rare state | Complex, high-frequency state | Medium complexity |
What Interviewers Actually Check
- Whether you can implement Context correctly with
createContext, a provider, anduseContext - Whether you know the re-render gotcha and the split-context mitigation
- Whether you can articulate what Redux adds that Context cannot provide
- Whether you know lighter alternatives like Zustand
- Whether you can provide a decision framework rather than a single "use this" answer
Follow-Up Questions
- How does
useSyncExternalStoreenable third-party libraries like Zustand to integrate with React's concurrent mode correctly? - Redux Toolkit's
createAsyncThunkhandles loading, success, and error states. How would you replicate this with Context anduseReducer? - What is the Jotai atomic state model and how does it differ from Redux's single-store model?
- How does React Query fit into the state management picture: what does it solve that both Context and Redux typically handle poorly?
- If you are using Redux and find that most state is server data (fetched from an API), what does that suggest about your architecture?
Common Candidate Mistakes
- Not knowing the re-render gotcha: every
useContextconsumer re-renders on any context value change - Using Redux for simple shared state in a small app ("use a sledgehammer to crack a nut")
- Treating Context as a performance optimization when its primary benefit is convenience (avoiding prop drilling)
- Not knowing about
useMemofor stabilizing the context value object - Not knowing about Zustand, Jotai, or other lightweight alternatives as a middle ground
Interview Readiness Checklist
Before you leave this question, make sure you can answer:
- Can you create a context, wrap a tree in a Provider, and read it with
useContext? - Can you explain why all
useContextconsumers re-render when the context value changes? - Can you describe at least two things Redux Toolkit provides that Context cannot?
- Can you demonstrate splitting contexts by update frequency?
- Can you articulate a decision framework for choosing between Context, Redux Toolkit, and Zustand?
Summary
React's Context API is a dependency injection mechanism that makes values accessible to any component in a subtree without prop drilling. The state is managed by useState or useReducer in the Provider; Context distributes it. Context is the right tool for values that are shared across many components and change infrequently: user session, theme, locale, feature flags.
The critical limitation is re-render granularity: every component that subscribes with useContext re-renders when any field in the context value changes. Mitigation is splitting contexts by update frequency so that infrequently-changed values do not trigger re-renders in consumers of frequently-changed values. For fine-grained subscriptions, Redux Toolkit's createSelector or Zustand's selector pattern are more appropriate.
Redux Toolkit adds the action-reducer model, async action middleware, time-travel debugging via DevTools, and memoized derived state via selectors. It is the right tool when state is complex, updates frequently from multiple sources, requires auditing, or needs DevTools for debugging. Zustand offers a middle ground: selector-based subscriptions and a simpler API than Redux without the overhead of reducers and action creators.
Does the Context API replace Redux?
For simple shared state, yes. For complex state with many frequent updates, derived state, middleware, or debugging requirements (time-travel, action log), Redux Toolkit remains the better tool. They are complementary, not direct replacements.
Advertisement
Stay Updated
Get the latest frontend challenges, interview questions and tutorials delivered to your inbox.
Advertisement