Context API vs Redux
React Context delivers a value to components; it does not manage state. See - with counted re-renders - why one big context re-renders every consumer, how to fix it, and when Context or Redux is the right choice.
What you will be able to do
- Explain what React Context does - and what it does not do
- Use createContext, a Provider and useContext, and avoid the missing-Provider crash
- Measure why one big context re-renders every consumer when any part changes
- Fix the two common Context performance problems: a new value object every render, and one context for everything
- Build a small "mini Redux" with Context and useReducer - and name what it is missing
- Use Context for what it is best at: values that rarely change, and injecting services
- Choose between Context and Redux for a given piece of state
The idea, in plain English
Lesson 5 showed Context as one of the ways out of prop drilling. Many developers then decide that Context "replaces Redux". This lesson shows why that is only half true - and the half that is false matters.
React Context is a way to deliver a value to every component below a Provider, without passing props through each level. That is all it does. It does not store the value, it does not decide how the value changes, and it does not let a component pick only the part it needs. The state itself still lives in useState or useReducer in some component; Context is the pipe that carries it down. That is why the outline calls Context dependency injection, not a state manager.
Redux is a state manager. It holds the state, decides how it may change (actions and reducers), records every change, and lets each component subscribe to exactly the piece it reads. The two tools solve different problems, and they are often used together.
All code below was run with React 19.3, react-redux 9.3.0 and Redux Toolkit 2.13.0, rendered and clicked in a test browser (jsdom). We counted every render.
Worked example: A context change re-rendering half the app: adding to the cart re-renders the user menu and the theme label.
1 - Click "Cart"
The cart changes from 0 items to 1. Only CartButton shows the cart. In a perfect world, only CartButton would re-render.
The same page - a user menu, a theme label, a cart button, a footer - built three ways. We clicked "Cart" once and counted who re-rendered.
An everyday picture: the power socket and the power station
A power socket in your wall delivers electricity to whatever you plug in. It does not make electricity. Somewhere far away, a power station makes it, decides how much to make, and keeps records. The socket only makes it easy to reach.
React Context is the socket. Any component below a Provider can "plug in" with useContext and receive the value. But the value is made and changed somewhere else - by useState or useReducer in a component. Redux is more like the power station with a meter on every house: it makes and controls the state, records every change, and each house is billed only for what it uses.
And the socket has one more limit: when the supply changes, every device plugged into it notices. That is the performance problem you will measure below.
How Context works, in three parts
createContext makes a context object, with a default value. A Provider, placed high in the tree, gives the context a value. Any component below it calls useContext and receives that value - no props in between.
If you forget the Provider, useContext quietly returns the default value. With a default of null, the component then crashes when it reads a field. We ran it: TypeError: Cannot read properties of null (reading ‘name’). A common habit is to write a small hook that throws a clear error when the Provider is missing.
import { createContext, useContext } from "react";
const UserContext = createContext(null); // 1. create it (default: null)
function UserMenu() {
const user = useContext(UserContext); // 3. read it - no Provider above: null
return <span>{user.name}</span>;
}
export default function App() {
return <UserMenu />; // 2. forgot <UserContext.Provider value={...}>
}
// Result:
// TypeError: Cannot read properties of null (reading 'name')function useUser() {
const user = useContext(UserContext);
if (user === null) {
throw new Error("useUser must be used inside <UserContext.Provider>");
}
return user;
}Example 1: one big context re-renders half the app
This is the example from the course outline. It is also what most first Context setups look like: one AppContext holding everything the app shares - the user, the theme, the cart, and a function to change the cart.
To make the test fair, every child is wrapped in memo. memo stops a component re-rendering just because its parent did. So if a child re-renders here, it is because the context changed.
We clicked "Cart" once. CartButton re-rendered - correct. But UserMenu and ThemeLabel also re-rendered, although the user and the theme did not change. The reason: useContext subscribes a component to the whole context value. When any part of it changes, every component that reads the context re-renders. There is no way to say "I only care about theme". Footer, which does not read the context, stayed at 0.
import { createContext, memo, useContext, useState } from "react";
const AppContext = createContext(null);
// memo: these components do not re-render because their PARENT re-renders.
// If they re-render, it is because the context changed.
const UserMenu = memo(function UserMenu() {
const { user } = useContext(AppContext);
return <span>{user.name}</span>;
});
const ThemeLabel = memo(function ThemeLabel() {
const { theme } = useContext(AppContext);
return <span>{theme}</span>;
});
const CartButton = memo(function CartButton() {
const { cart, addToCart } = useContext(AppContext);
return <button onClick={() => addToCart("Redux")}>Cart ({cart.length})</button>;
});
const Footer = memo(function Footer() {
return <footer>© 2026</footer>; // does not read the context
});
export default function App() {
const [user] = useState({ name: "Ravi" });
const [theme] = useState("dark");
const [cart, setCart] = useState([]);
const addToCart = (item) => setCart([...cart, item]);
return (
<AppContext.Provider value={{ user, theme, cart, addToCart }}>
<UserMenu />
<ThemeLabel />
<CartButton />
<Footer />
</AppContext.Provider>
);
}App 1
UserMenu 1 <- does not use the cart
ThemeLabel 1 <- does not use the cart
CartButton 1
Footer 0 (does not read the context)Problem 2: a new value object on every render
There is a second, quieter problem. value={{ theme }} creates a brand-new object every time the Provider’s component renders. React compares the old and new value by identity - is it the same object? A new object is never the same, so every consumer re-renders, even if theme did not change.
We tested it with a button that changes an unrelated counter in App. Three clicks: ThemeLabel re-rendered 3 times. Then we wrapped the value in useMemo, so the same object is reused until theme really changes. Three clicks: ThemeLabel re-rendered 0 times.
import { createContext, memo, useContext, useMemo, useState } from "react";
const ThemeContext = createContext(null);
const ThemeLabel = memo(function ThemeLabel() {
const { theme } = useContext(ThemeContext);
return <span>{theme}</span>;
});
export function AppNewObject() {
const [theme] = useState("dark");
const [clicks, setClicks] = useState(0); // unrelated to the theme
return (
<ThemeContext.Provider value={{ theme }}> {/* a NEW object every render */}
<button onClick={() => setClicks(clicks + 1)}>Clicked {clicks}</button>
<ThemeLabel />
</ThemeContext.Provider>
);
}
export function AppMemoValue() {
const [theme] = useState("dark");
const [clicks, setClicks] = useState(0);
const value = useMemo(() => ({ theme }), [theme]); // the SAME object until theme changes
return (
<ThemeContext.Provider value={value}>
<button onClick={() => setClicks(clicks + 1)}>Clicked {clicks}</button>
<ThemeLabel />
</ThemeContext.Provider>
);
}AppNewObject ThemeLabel renders: 3
AppMemoValue ThemeLabel renders: 0Fix: split the context
The usual fix for one big context is to split it: one context per kind of value. Now a component reads only the context it needs, and only that context’s changes reach it. The cart value is wrapped in useMemo so it changes only when the cart changes.
We clicked "Cart" again. UserMenu and ThemeLabel stayed at 0; only CartButton (and App, which owns the state) re-rendered. The cost: three Providers instead of one, and a little more code for every new kind of value. In a big app this becomes a stack of Providers at the top - which is part of why teams start to look at Redux.
import { createContext, memo, useContext, useMemo, useState } from "react";
const UserContext = createContext(null);
const ThemeContext = createContext(null);
const CartContext = createContext(null);
const UserMenu = memo(function UserMenu() {
const user = useContext(UserContext);
return <span>{user.name}</span>;
});
const ThemeLabel = memo(function ThemeLabel() {
const theme = useContext(ThemeContext);
return <span>{theme}</span>;
});
const CartButton = memo(function CartButton() {
const { cart, addToCart } = useContext(CartContext);
return <button onClick={() => addToCart("Redux")}>Cart ({cart.length})</button>;
});
const Footer = memo(function Footer() {
return <footer>© 2026</footer>;
});
export default function App() {
const [user] = useState({ name: "Ravi" });
const [theme] = useState("dark");
const [cart, setCart] = useState([]);
const cartValue = useMemo(
() => ({ cart, addToCart: (item) => setCart((c) => [...c, item]) }),
[cart],
);
return (
<UserContext.Provider value={user}>
<ThemeContext.Provider value={theme}>
<CartContext.Provider value={cartValue}>
<UserMenu />
<ThemeLabel />
<CartButton />
<Footer />
</CartContext.Provider>
</ThemeContext.Provider>
</UserContext.Provider>
);
}App 1 (owns the state)
UserMenu 0
ThemeLabel 0
CartButton 1
Footer 0The same page with Redux
Now the same four components with a Redux store. Each one uses useSelector to pick exactly the piece it shows: state.user.name, state.theme, state.cart.items.length. The store keeps track of who selected what.
One click on "Cart": only CartButton re-rendered. UserMenu and ThemeLabel stayed at 0 - without memo, without splitting anything, and without useMemo. App also stayed at 0, because the state is not in App any more; it is in the store. This "read only one field" ability is the biggest practical difference between Context and Redux.
import { configureStore, createSlice } from "@reduxjs/toolkit";
import { Provider, useDispatch, useSelector } from "react-redux";
const cartSlice = createSlice({
name: "cart",
initialState: { items: [] },
reducers: {
added(state, action) {
state.items.push(action.payload);
},
},
});
const store = configureStore({
reducer: {
user: () => ({ name: "Ravi" }), // tiny reducers that never change, for the example
theme: () => "dark",
cart: cartSlice.reducer,
},
});
function UserMenu() {
const name = useSelector((state) => state.user.name);
return <span>{name}</span>;
}
function ThemeLabel() {
const theme = useSelector((state) => state.theme);
return <span>{theme}</span>;
}
function CartButton() {
const count = useSelector((state) => state.cart.items.length);
const dispatch = useDispatch();
return <button onClick={() => dispatch(cartSlice.actions.added("Redux"))}>Cart ({count})</button>;
}
function Footer() {
return <footer>© 2026</footer>;
}
export default function App() {
return (
<Provider store={store}>
<UserMenu />
<ThemeLabel />
<CartButton />
<Footer />
</Provider>
);
}App 0
UserMenu 0
ThemeLabel 0
CartButton 1
Footer 0One big contextApp 1, UserMenu 1, ThemeLabel 1, CartButton 1, Footer 0Split contextsApp 1, UserMenu 0, ThemeLabel 0, CartButton 1, Footer 0ReduxApp 0, UserMenu 0, ThemeLabel 0, CartButton 1, Footer 0"Context + useReducer is Redux" - almost
You may read that Context plus useReducer gives you Redux without the library. It is a good exercise, and it shows the division of work clearly: useReducer manages the state - a reducer function and actions, like Redux - and Context only delivers state and dispatch to the components. We ran it: Cart (0) -> Cart (1) -> Cart (2).
What it does not give you is everything around that core. Every consumer re-renders on every change (no useSelector). There are no Redux DevTools to see each action and step back through them. There is no middleware for logging, async logic or analytics. And there is no RTK Query for server data (Lesson 3). For a small feature it is a fine choice; for app-wide state these missing pieces are exactly what Redux adds.
import { createContext, useContext, useReducer } from "react";
// useReducer MANAGES the state. Context only DELIVERS it.
function cartReducer(state, action) {
switch (action.type) {
case "cart/added":
return { items: [...state.items, action.payload] };
default:
return state;
}
}
const CartContext = createContext(null);
function CartButton() {
const { state, dispatch } = useContext(CartContext);
return (
<button onClick={() => dispatch({ type: "cart/added", payload: "Redux" })}>
Cart ({state.items.length})
</button>
);
}
export default function App() {
const [state, dispatch] = useReducer(cartReducer, { items: [] });
return (
<CartContext.Provider value={{ state, dispatch }}>
<CartButton />
</CartContext.Provider>
);
}
// clicked twice: Cart (0) -> Cart (1) -> Cart (2)What Context is great at: injecting things
Context shines when the value changes rarely or never: the theme, the language, the logged-in user, feature flags. Few changes means the "every consumer re-renders" cost almost never happens.
It is also the best tool for dependency injection - giving components a service they should use, without hard-coding it. Below, CourseList asks the context for "whatever API I should use". In the real app that is realApi, which calls the server. In a test or a design preview, a Provider injects fakeApi instead, and CourseList works with no server at all. We rendered TestApp and the list showed Python and Redux from the fake. Redux is not designed for this; Context is.
import { createContext, useContext, useEffect, useState } from "react";
// The real service: talks to the server
const realApi = {
async getCourses() {
const response = await fetch("/api/courses");
return response.json();
},
};
// The context holds a SERVICE, not changing state
export const ApiContext = createContext(realApi);
export function CourseList() {
const api = useContext(ApiContext); // "give me whatever API I should use"
const [courses, setCourses] = useState([]);
useEffect(() => {
api.getCourses().then(setCourses);
}, [api]);
return <ul>{courses.map((c) => <li key={c}>{c}</li>)}</ul>;
}
// In a test or a Storybook story: inject a fake, no server needed
const fakeApi = { getCourses: async () => ["Python", "Redux"] };
export function TestApp() {
return (
<ApiContext.Provider value={fakeApi}>
<CourseList />
</ApiContext.Provider>
);
}
// rendered TestApp -> list: Python, ReduxContext or Redux? A side-by-side
Neither tool is "better". They answer different questions. Context answers "how do I get this value to components far below without props?". Redux answers "how do I manage state that many parts of the app read and change, with clear rules and a record of every change?".
What it isContext: a way to deliver a value. Redux: a state manager.Where the state livesContext: in useState / useReducer of some component. Redux: in the store.Read one field onlyContext: no - every consumer re-renders on any change. Redux: yes - useSelector.Rules for changesContext: whatever you write. Redux: actions and reducers, always.See every changeContext: no. Redux: Redux DevTools and middleware.Server dataContext: write it yourself. Redux: RTK Query.SetupContext: built into React, very little code. Redux: a library and more code.Best forContext: theme, language, auth user, injected services. Redux: frequently changing state shared across the app.Using both together
In real apps the question is rarely "Context or Redux" for the whole app. A typical React app with Redux still uses Context: the theme and the language in Context, injected services in Context, and the shared, frequently changing data - cart, notifications, editor state - in Redux. In fact react-redux itself uses Context inside <Provider store={store}> to give every component access to the store; useSelector then adds the "subscribe to one field" part on top.
Common questions
Is Context slow? Not by itself. It re-renders every consumer when its value changes. If the value rarely changes - a theme - that cost almost never happens. It becomes a problem when a frequently changing value, like a cart or form text, sits in a context with many consumers.
Can I avoid the extra re-renders and still use Context? Partly: split contexts, keep the value stable with useMemo, and wrap children in memo. Each fix adds code, and none of them lets a component read only one field of an object - that needs a selector.
Does Redux use Context? Yes. The react-redux Provider puts the store in a context. The store object itself never changes, so that context never causes re-renders; the subscriptions made by useSelector decide who re-renders.
So when do I need Redux? Lesson 8 answers that with honest criteria - including cases where you should delete a store you already have.
This lesson at a glance
createContextMake a context with a default value.
const ThemeContext = createContext("light")ProviderGive the context a value for everything below.
<ThemeContext.Provider value={theme}>useContextRead the value from the nearest Provider above.
const theme = useContext(ThemeContext)
Stable valueReuse the same object until its contents change.
useMemo(() => ({ theme }), [theme])useReducerManage state with a reducer and actions.
const [state, dispatch] = useReducer(reducer, init)
useSelectorRedux: read one field; re-render only when it changes.
useSelector((state) => state.cart.items.length)
Try it yourself
The code does not change. Swap the content string and the program does something else entirely.
“In OneBig.jsx, add console.log("ThemeLabel render") to ThemeLabel and click Cart a few times.”
“In NewObject.jsx, click the unrelated button in both versions and count ThemeLabel renders.”
“Remove the Provider from Split.jsx. What error do you get? Then add the safer useUser hook.”
“In Inject.jsx, make fakeApi return three courses, or make it throw an error. How does CourseList behave?”
What usually goes wrong
Every consumer re-renders when any part changes. Split by kind of value, or move frequently changing data to a store.
✗ <AppContext.Provider value={{ user, theme, cart, addToCart }}>✓ <UserContext.Provider value={user}> ... <CartContext.Provider value={cartValue}>value={{ theme }} is a new object each time, so every consumer re-renders even when theme did not change.
✗ <ThemeContext.Provider value={{ theme }}>✓ const value = useMemo(() => ({ theme }), [theme]);
<ThemeContext.Provider value={value}>useContext silently returns the default value. With null, the app crashes when a field is read.
✗ const user = useContext(UserContext); // null without a Provider✓ const user = useUser(); // throws a clear error if the Provider is missingContext only delivers. The state is still managed by useState or useReducer - so the rules, the records and the selective updates are yours to build.
Practice
Write these yourself before opening anything. Getting them wrong first is most of how this sticks.
In OneBig.jsx, which components re-rendered after one click on Cart, and which of those re-renders were unnecessary?
Show hintHide hint
Which components read the cart?
Show solutionHide solution
Re-rendered: App, UserMenu, ThemeLabel, CartButton.
Unnecessary: UserMenu and ThemeLabel - they read user and theme,
which did not change. They re-rendered because they read the same
context, and the context value changed.Choose Context or Redux for each: (a) the current language, (b) a list of open chat windows changed from many places, (c) an analytics service object, (d) the theme, (e) the items in a shopping cart shown in the header and changed on every product page.
Show hintHide hint
How often does it change, and is it a value or a service?
Show solutionHide solution
(a) Context - rarely changes
(b) Redux - changes often, read and changed from many places
(c) Context - a service to inject
(d) Context - rarely changes
(e) Redux - changes often, shared widelyRewrite this Provider so ThemeLabel does not re-render when App re-renders for another reason: <ThemeContext.Provider value={{ theme, setTheme }}>
Show hintHide hint
Keep the same object until theme changes.
Show solutionHide solution
const value = useMemo(() => ({ theme, setTheme }), [theme]);
<ThemeContext.Provider value={value}>
...
</ThemeContext.Provider>
// setTheme from useState never changes, so [theme] is enough.Fix one big context
Start from OneBig.jsx. Without Redux, change it so that clicking Cart re-renders only CartButton (and App). Then write two sentences on what Redux would have given you for free.
- User, theme and cart in separate contexts
- The cart context value is stable (useMemo) and changes only when the cart changes
- Measured after one click: UserMenu 0, ThemeLabel 0, CartButton 1
- Explain in two sentences what useSelector does that your solution does not
Show one solutionHide solution
The code is Split.jsx from this lesson:
- UserContext, ThemeContext, CartContext instead of one AppContext
- cartValue = useMemo(() => ({ cart, addToCart }), [cart])
- children wrapped in memo
What Redux would give for free:
useSelector lets each component read just one field of one store, so no
splitting, memo or useMemo is needed to stop unrelated re-renders - and App
would not re-render either, because the state would live in the store.
It also records every change as an action you can inspect in Redux DevTools.Key points
- Context delivers a value to components below a Provider; it does not manage state.
- The state still lives in useState or useReducer; Context is the pipe - dependency injection.
- Without a Provider, useContext returns the default value - often null, and a crash.
- Every consumer re-renders when the context value changes; you cannot read just one field.
- One big context re-rendered UserMenu and ThemeLabel on a cart change; split contexts and Redux did not.
- A new value object every render re-renders all consumers; keep it stable with useMemo.
- Context + useReducer is a small Redux without selectors, DevTools, middleware or RTK Query.
- Use Context for rarely changing values and injected services; Redux for frequently changing shared state - often both together.
Quick check before you move on
Interview questions
Is the Context API a replacement for Redux?
Not in general. Context is a dependency injection mechanism - it delivers a value through the tree. It does not manage state, offer selective subscriptions, enforce update rules, or provide tooling. Combined with useReducer it covers simple shared state; Redux adds selectors, DevTools, middleware and RTK Query.
What is the main performance pitfall of Context?
Every consumer re-renders whenever the provided value changes, and consumers cannot subscribe to part of it. A large context with frequently changing data, or a new value object created on every render, causes widespread re-renders.
How do you reduce Context re-renders?
Split contexts by concern, memoise the provided value with useMemo, separate state from dispatch, and memoise children where needed. For frequently changing shared state, a store with selectors is usually simpler.
When is Context the right tool?
For values that change rarely and are needed widely - theme, locale, authenticated user, feature flags - and for injecting services or configuration that tests can replace.
How does react-redux avoid the Context re-render problem?
It puts the store - an object that never changes - in context, so the context never triggers renders. Components subscribe to the store through useSelector and re-render only when their selected value changes.
Quiz
- 1.
One click on Cart: renders with one big context, split contexts, and Redux?
- 2.
Three clicks on an unrelated button: how many times did ThemeLabel re-render with value={{ theme }}, and with useMemo?
- 3.
What does Context + useReducer lack compared with Redux?
- 4.
Does react-redux use Context?
- 5.
Why is Context good for injecting an API client?
Comments
Sign in to leave a comment. Your name and photo come from Google; nothing else is shared.
Loading comments...
AI
System Design
Backend
- GraphQL8 modules · 69 lessons planned
- Core Python13 modules · 75 lessons planned
- FastAPI5 sections · 20 lessons
- Node.js14 modules · 206 lessons planned
- Node.js Performance7 chapters · 36 topics
- Event Loop Lifecycle6 phases · 3 scenarios
- Docker & Containerization11 modules · 144 lessons planned
- AWS for Developers14 modules · 219 lessons planned
- CI/CD & DevOps Automation10 modules · 134 lessons planned