React Context API vs Redux: When You Actually Need State Management
A junior dev on your team just installed Redux, Redux Toolkit, and redux-persist to share a single boolean — whether a modal is open — between two sibling components three levels apart. This happens constantly, and it's usually the wrong call in both directions: sometimes Context is overkill for what prop drilling would solve in five minutes, and sometimes Redux is overkill for what Context would solve just as well.
What each tool is actually for
Context is React's built-in way to avoid prop drilling. It re-renders every consumer when the value changes — there's no selective subscription. Redux (or Zustand, Jotai, etc.) is a state container with its own update model, devtools, middleware, and — critically — selective subscriptions, so a component only re-renders when the specific slice of state it reads changes.
// Context: simple, built-in, no selective re-renders
const ThemeContext = createContext("light");
function App() {
const [theme, setTheme] = useState("light");
return (
<ThemeContext.Provider value={theme}>
<Toolbar />
</ThemeContext.Provider>
);
}
function Toolbar() {
const theme = useContext(ThemeContext);
return <div className={theme}>...</div>;
}
Every component under ThemeContext.Provider that calls useContext(ThemeContext) re-renders whenever theme changes — even if they only care about one field in a larger context value.
Where Context breaks down
Put frequently-changing state (form input on every keystroke, a websocket feed, mouse position) into Context and watch every consumer re-render on every update, regardless of whether they read that specific piece of data:
const AppState = createContext();
function AppProvider({ children }) {
const [state, setState] = useState({ user: null, cart: [], notifications: [] });
return <AppState.Provider value={{ state, setState }}>{children}</AppState.Provider>;
}
Now a component that only reads state.user re-renders every time state.cart changes, because Context doesn't do partial subscriptions — it's all-or-nothing on the object identity.
Where Redux (or Zustand) earns its keep
import { create } from "zustand";
const useCartStore = create((set) => ({
items: [],
addItem: (item) => set((s) => ({ items: [...s.items, item] })),
}));
// Only re-renders when `items` changes, not on unrelated state
function CartBadge() {
const items = useCartStore((s) => s.items);
return <span>{items.length}</span>;
}
That selector-based subscription (useCartStore((s) => s.items)) is the actual value proposition — components only re-render for the exact slice they read. Redux Toolkit gives you the same guarantee via useSelector, plus a mature devtools/middleware ecosystem (time-travel debugging, logging, persistence) that Context has none of.
A decision rule that actually works
- Rarely-changing, app-wide config (theme, locale, auth user): Context is fine, arguably ideal — it's built in, zero dependencies, and re-render cost is negligible since it barely changes.
- Frequently-updating shared state read by many components (cart contents, live data feeds, collaborative editing state): reach for a state library with selective subscriptions.
- State only two or three components need: neither — lift it to the nearest common parent and pass props. This is still the simplest, most debuggable option and gets dismissed too often.
Common mistakes
- Reaching for Redux before checking whether prop drilling for 2-3 levels is actually a problem — it usually isn't.
- Putting everything into one giant Context object, which forces every consumer to re-render on any change anywhere in that object.
- Never splitting Context into multiple providers (e.g.,
UserContext,CartContext) — smaller, focused contexts limit the blast radius of re-renders. - Adding Redux Toolkit + Redux Persist + middleware for state that changes twice per session.
What I'd actually use
Zustand over Redux Toolkit for new projects unless you specifically need Redux's devtools ecosystem or your team already has deep Redux investment — it does the same job with drastically less boilerplate. Reserve Context for state that's genuinely global and infrequently updated. Everything else, start with local state and props; you can always lift it later.
Next steps
Audit your app's Context providers — if any of them wrap state that changes on every keystroke or every websocket message, that's your signal to move it to a selector-based store instead.