A practical guide to 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.
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.
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.
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.
UserContext, CartContext) — smaller, focused contexts limit the blast radius of re-renders.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.
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.