⚡ AMP
React

React Context API vs Redux: when you actually need state management

A practical guide to react Context API vs Redux: when you actually need state management.

Nitheesh DR 4 min read

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

Common mistakes

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.