Concept
Both Redux and React's Context API let state live outside a single component and be shared across a tree, but they solve genuinely different problems, and confusing them (using Context for what Redux does well, or vice versa) is a common source of real, measurable performance and architecture problems. This topic is a direct, mechanical comparison, not an opinion piece, every claim here traces back to specific, already-confirmed behavior from Context API and Selectors & Reselect.
The core mechanical difference: re-render granularity
// Context: ANY value change re-renders EVERY consumer
const AppContext = createContext();
function App() {
const [state, setState] = useState({ user: null, cart: [], theme: "dark" });
return <AppContext.Provider value={state}>{/* ... */}</AppContext.Provider>;
}
function ThemeToggle() {
const { theme } = useContext(AppContext); // re-renders even when ONLY `cart` changes
return <button>{theme}</button>;
}// Redux: useSelector re-renders ONLY components whose selected slice changed
function ThemeToggle() {
const theme = useSelector((state) => state.theme); // does NOT re-render when `cart` changes
return <button>{theme}</button>;
}This is the single most important, concrete distinction, and it's already been confirmed on both sides in this course: Context API established that every consumer of a context re-renders whenever the provided value changes at all, regardless of which specific field a given consumer actually reads, the only workaround is manually splitting one large context into several smaller, independently-provided contexts. Selectors & Reselect confirmed the opposite behavior for Redux: useSelector's reference-equality check means a component reading state.theme is never notified when an unrelated slice like state.cart changes, with no manual context-splitting required, the granularity is automatic, per-component, built into the base mechanism.
store.dispatch({ type: 'counter/incremented' });
dispatch() sends the action to the store's single root reducer, this is the ONLY way state changes in Redux; nothing else can mutate the store.
Where each one genuinely wins
Context wins for: state that's genuinely low-frequency-changing and doesn't need cross-cutting selector logic, theme, locale, authenticated user identity, feature flags. It requires zero external dependency, ships with React itself, and for state that rarely changes, the "every consumer re-renders" cost is negligible since re-renders are rare to begin with.
Redux wins for: state that changes frequently, is read by many differently-scoped components, or needs the tooling Redux (and RTK) provide, Redux DevTools' full action-log time-travel debugging, middleware for cross-cutting async/logging concerns (covered in Middleware), normalized entity management (covered in Data Normalization and Entity Adapter), and RTK Query's caching layer (covered in RTK Query), none of which Context provides at all; it's purely a value-passing mechanism, with no built-in action log, middleware system, or caching.
A common misconception: "Redux is just Context with extra steps"
This misconception specifically ignores the re-render granularity difference confirmed above, Context, even used correctly with a well-designed useContext hook, fundamentally cannot replicate useSelector's per-component, per-slice re-render skipping without manually splitting into many small contexts (one per independently-changing piece of state), which, once you have enough independent pieces of state, becomes its own significant maintenance burden, arguably more ceremony than Redux's dispatch/reducer pattern for comparable scale. Redux isn't "Context plus boilerplate", it's a fundamentally different re-render model, plus a middleware system, plus (via RTK) a caching layer, none of which Context provides.
Using both together, deliberately
// Context for rarely-changing, cross-cutting identity/config:
<ThemeProvider><LocaleProvider><ReduxProvider store={store}>
<App /> {/* Redux for frequently-changing, selector-driven app data */}
</ReduxProvider></LocaleProvider></ThemeProvider>These aren't mutually exclusive, a real app commonly uses Context for genuinely static-ish, cross-cutting concerns (theme, locale) alongside Redux for the actual, frequently-changing application data, using each where its specific strengths apply rather than treating the choice as all-or-nothing.
Try It
Predict the outcome before checking the solution.
// Scenario A: Context
const AppContext = createContext();
function CartCount() {
const { cart } = useContext(AppContext);
console.log("CartCount rendered");
return <span>{cart.length}</span>;
}
// theme changes elsewhere in the SAME context value
// Scenario B: Redux
function CartCount() {
const cart = useSelector((state) => state.cart);
console.log("CartCount rendered");
return <span>
In both scenarios, only theme changes, never cart. Does CartCount log a render in Scenario A? In Scenario B?
Solution
Scenario A: yes, it re-renders. Scenario B: no, it does not.
This is the exact confirmed mechanical difference from both source topics. In Scenario A, CartCount consumes the entire context value via useContext(AppContext), Context has no way to know CartCount only actually reads cart, so any change to the provided value object (including an unrelated theme field) triggers every consumer's re-render, confirmed in Context API. In Scenario B, useSelector((state) => state.cart) subscribes specifically to the cart slice, confirmed in Selectors & Reselect, a dispatch that only changes state.theme leaves state.cart's reference untouched, so 's selector output is unchanged, and React-Redux correctly skips re-rendering it. Same intent (a component that only cares about ), genuinely different outcome, purely from the underlying re-render mechanism.
Implement It Yourself
Sketch a version of Context that regains selector-like granularity, to see exactly what extra work is required to approximate what Redux provides by default:
// Approximating useSelector's behavior on top of Context, by hand:
function useContextSelector(context, selector) {
const fullValue = useContext(context); // still re-renders on ANY context value change...
const selected = selector(fullValue);
const ref = useRef(selected);
const [, forceRender] = useReducer((c) => c + 1, 0);
useEffect(() => {
if (!Object.is(ref.current, selected)) {
ref.current = selected;
}
});
This is a genuinely important, often-missed nuance: even a careful, hand-rolled "selector" wrapper around useContext cannot fully replicate useSelector's render-skipping behavior, because useContext itself already subscribed the component to every change in the provided value, you can prevent using a stale value, but not prevent the re-render from being triggered in the first place, without stepping outside Context's built-in subscription model entirely (exactly what Zustand and Redux's useSelector do, each in their own way).
Under the Hood
This comparison directly connects Context API's confirmed all-consumers-re-render behavior with Selectors & Reselect's confirmed per-slice re-render skipping, both mechanisms were established independently earlier in this course, and this topic is where they're placed side by side. It also connects to Zustand, which achieves selector-based granularity via a different mechanism (an external store, not React Context), worth contrasting against Redux specifically in Redux vs Zustand vs MobX vs Jotai.
Common Mistakes
1. Using Context for frequently-changing, high-fanout application state
// ❌ a single AppContext holding cart, user, notifications, filters, ALL rapidly changing,
// consumed by dozens of components throughout the treeThis is precisely the scenario where Context's all-consumers-re-render behavior becomes a real, measurable performance problem, every one of those dozens of components re-renders on every single change to any piece of this shared state, regardless of relevance.
2. Reaching for Redux for genuinely simple, rarely-changing, narrowly-scoped state
// ❌ configureStore + createSlice + a Provider, JUST for a theme toggle used by 3 componentsFor state this narrow and infrequently-changing, Redux's setup ceremony (store, slice, Provider, dispatch/selector usage) provides no meaningful benefit over a simple Context, the re-render granularity difference barely matters when changes and consumer count are both small.
3. Assuming splitting one Context into several smaller ones is "free" compared to Redux's selector model
// Manually splitting into ThemeContext, CartContext, UserContext, NotificationsContext, ...
// as the app's independent state pieces GROW, requiring a NEW provider wrapper each timeThis works, but scales linearly in manual effort with the number of independent state pieces, a new context, a new provider, updated wrapping, whereas Redux's useSelector granularity is automatic per-component, requiring no equivalent per-slice provider setup as the app's state surface grows.
Best Practices
- Default to Context for genuinely low-frequency, cross-cutting state (theme, locale, auth identity) that doesn't need fine-grained selector logic or Redux's tooling.
- Default to Redux (or an external-store library like Zustand) for frequently-changing, widely-consumed application state, where the automatic re-render granularity and available tooling (DevTools, middleware, caching) provide real, compounding value.
- Use both together deliberately in the same app, they solve different problems, and there's no rule requiring an all-or-nothing choice.
Performance Tips
- The re-render granularity difference compounds with app size, a small app with few consumers and infrequent changes may never notice Context's all-consumers-re-render behavior as a real cost; a large app with many independent state slices and many consumers absolutely will.
- Manually splitting Context to approximate selector-like granularity is a valid mitigation, but its own maintenance cost (a new context + provider per independent piece of state) grows linearly with the number of state pieces, worth comparing directly against Redux's flat, no-additional-provider-per-slice cost before committing to a heavily-split-Context architecture.
