Context API vs Redux: When to Use Each

Intermediate12 min interview
Skills tested:
Creating and consuming a React context with createContext, Provider, and useContextUnderstanding the performance gotcha: all useContext consumers re-render on any context value changeKnowing what Redux adds beyond Context: action-reducer model, middleware, DevTools, derived state selectorsMaking the decision between Context, Redux Toolkit, and lighter alternatives like ZustandSplitting contexts by update frequency to avoid unnecessary re-renders

Advertisement

🧩 Scenario

State management choice is a common architecture question in interviews. The right answer depends on the size of the shared state, update frequency, the need for middleware (logging, async actions), derived state complexity, and team familiarity. Knowing the tradeoffs and being able to articulate a decision framework is more valuable than knowing one tool deeply.

Architecture Walkthrough

What Context Provides

React's Context API solves one problem: sharing values across a component tree without passing props through every intermediate component. You create a context with createContext, wrap a subtree in a Provider with a value, and any descendant reads the value with useContext. The component does not need to know about the depth of the tree or the components between it and the provider.

Context is not a state manager. It is a dependency injection mechanism. The state itself is managed by useState or useReducer in the Provider component; Context just makes that state accessible to any descendant without prop drilling.

The Re-Render Gotcha

Every component that calls useContext(MyContext) subscribes to the entire context value. When the context value object changes (even if only one field in it changed), all subscribers re-render. This is a significant difference from Redux, where selectors allow components to subscribe to specific slices of state.

The mitigation is to split contexts by update frequency. State that rarely changes (user session, theme) goes in one context. State that updates frequently (cart, notifications) goes in another. Components that only read the user session are not affected by cart updates.

What Redux Toolkit Adds

Redux Toolkit adds the action-reducer model: state changes are described as dispatched actions, and reducers are pure functions that produce new state from an action. This enforces a predictable, auditable state machine. Middleware (Redux Thunk or Redux Saga) handles async actions, including loading and error states, as part of the action lifecycle. The Redux DevTools extension provides time-travel debugging, action history, and state diffs. Selectors with createSelector compute derived state and memoize it, preventing unnecessary re-renders.

Context with useState provides none of these. It is the right tool for simple, infrequently updated global state.


Key Code Explained

// Context: create, provide, consume
interface UserContextValue {
  user: User | null;
  signOut: () => void;
}

const UserContext = createContext<UserContextValue | null>(null);

function useUser() {
  const ctx = useContext(UserContext);
  if (!ctx) throw new Error('useUser must be inside UserProvider');
  return ctx;
}

function UserProvider({ children }: { children: React.ReactNode }) {
  const [user, setUser] = useState<User | null>(null);

  const signOut = useCallback(() => {
    setUser(null);
    clearAuthToken();
  }, []);

  // The value object must be stable — useMemo prevents recreation on every render
  const value = useMemo(() => ({ user, signOut }), [user, signOut]);

  return <UserContext.Provider value={value}>{children}</UserContext.Provider>;
}

// Consumer: reads user context
function NavBar() {
  const { user, signOut } = useUser();
  return (
    <nav>
      <span>{user?.name}</span>
      <button onClick={signOut}>Sign Out</button>
    </nav>
  );
}


// Split contexts by update frequency to avoid cross-concern re-renders
function AppProviders({ children }: { children: React.ReactNode }) {
  return (
    // UserContext: session changes rarely
    <UserProvider>
      {/* CartContext: updates on every add/remove/quantity change */}
      <CartProvider>
        {/* NotificationContext: updates on every new notification */}
        <NotificationProvider>
          {children}
        </NotificationProvider>
      </CartProvider>
    </UserProvider>
  );
}
// NavBar reads UserContext → only re-renders on session changes
// CartIcon reads CartContext → only re-renders on cart changes


// Redux Toolkit comparison: slice-based state
import { createSlice, configureStore } from '@reduxjs/toolkit';

const cartSlice = createSlice({
  name: 'cart',
  initialState: { items: [] as CartItem[], total: 0 },
  reducers: {
    addItem(state, action: PayloadAction<CartItem>) {
      state.items.push(action.payload); // Immer makes this safe
      state.total += action.payload.price;
    },
    removeItem(state, action: PayloadAction<string>) {
      const index = state.items.findIndex((i) => i.id === action.payload);
      if (index >= 0) {
        state.total -= state.items[index].price;
        state.items.splice(index, 1);
      }
    },
  },
});

// Selector: only re-renders consumer when count changes, not on price changes
const selectCartCount = (state: RootState) => state.cart.items.length;

function CartIcon() {
  const count = useSelector(selectCartCount); // fine-grained subscription
  return <span>{count} items</span>;
}

The useMemo on the context value in UserProvider is important. Without it, the value object is recreated on every render of UserProvider, even when user and signOut have not changed. Because context comparison uses reference equality, a new object reference triggers re-renders in all consumers even when the actual data is identical.


Tradeoffs

FeatureContext + useStateRedux ToolkitZustand
Setup complexityNoneModerateLow
Fine-grained subscriptionsNo (must split)Yes (selectors)Yes (selectors)
Middleware / async actionsNo built-inYes (Thunk)Yes (simple)
DevTools / time-travelNoYesBasic
Bundle sizeZero~10KB gzipped~1KB gzipped
Best forSimple, rare stateComplex, high-frequency stateMedium complexity

What Interviewers Actually Check

  • Whether you can implement Context correctly with createContext, a provider, and useContext
  • Whether you know the re-render gotcha and the split-context mitigation
  • Whether you can articulate what Redux adds that Context cannot provide
  • Whether you know lighter alternatives like Zustand
  • Whether you can provide a decision framework rather than a single "use this" answer

Follow-Up Questions

  1. How does useSyncExternalStore enable third-party libraries like Zustand to integrate with React's concurrent mode correctly?
  2. Redux Toolkit's createAsyncThunk handles loading, success, and error states. How would you replicate this with Context and useReducer?
  3. What is the Jotai atomic state model and how does it differ from Redux's single-store model?
  4. How does React Query fit into the state management picture: what does it solve that both Context and Redux typically handle poorly?
  5. If you are using Redux and find that most state is server data (fetched from an API), what does that suggest about your architecture?

Common Candidate Mistakes

  • Not knowing the re-render gotcha: every useContext consumer re-renders on any context value change
  • Using Redux for simple shared state in a small app ("use a sledgehammer to crack a nut")
  • Treating Context as a performance optimization when its primary benefit is convenience (avoiding prop drilling)
  • Not knowing about useMemo for stabilizing the context value object
  • Not knowing about Zustand, Jotai, or other lightweight alternatives as a middle ground

Interview Readiness Checklist

Before you leave this question, make sure you can answer:

  • Can you create a context, wrap a tree in a Provider, and read it with useContext?
  • Can you explain why all useContext consumers re-render when the context value changes?
  • Can you describe at least two things Redux Toolkit provides that Context cannot?
  • Can you demonstrate splitting contexts by update frequency?
  • Can you articulate a decision framework for choosing between Context, Redux Toolkit, and Zustand?

Summary

React's Context API is a dependency injection mechanism that makes values accessible to any component in a subtree without prop drilling. The state is managed by useState or useReducer in the Provider; Context distributes it. Context is the right tool for values that are shared across many components and change infrequently: user session, theme, locale, feature flags.

The critical limitation is re-render granularity: every component that subscribes with useContext re-renders when any field in the context value changes. Mitigation is splitting contexts by update frequency so that infrequently-changed values do not trigger re-renders in consumers of frequently-changed values. For fine-grained subscriptions, Redux Toolkit's createSelector or Zustand's selector pattern are more appropriate.

Redux Toolkit adds the action-reducer model, async action middleware, time-travel debugging via DevTools, and memoized derived state via selectors. It is the right tool when state is complex, updates frequently from multiple sources, requires auditing, or needs DevTools for debugging. Zustand offers a middle ground: selector-based subscriptions and a simpler API than Redux without the overhead of reducers and action creators.

Frequently Asked Questions

Does the Context API replace Redux?

For simple shared state, yes. For complex state with many frequent updates, derived state, middleware, or debugging requirements (time-travel, action log), Redux Toolkit remains the better tool. They are complementary, not direct replacements.

Advertisement


Stay Updated

Get the latest frontend challenges, interview questions and tutorials delivered to your inbox.

Advertisement