Skip to main content

What is the "Context + Reducer" pattern?

The essence of the pattern

We combine Context (for global access to data) and useReducer (for predictable state management through actions).

Context is "a way to share state". The reducer is the "brain" that says how the state changes.


Basic idea

javascript
// Context: available to all components const CounterContext = createContext(); // Reducer: manages state changes function counterReducer(state, action) { switch (action.type) { case "increment": return { count: state.count + 1 }; case "decrement": return { count: state.count - 1 }; default: throw new Error("Unknown action"); } }

Implementing the pattern

1. Provider (wraps the whole app)

javascript
function CounterProvider({ children }) { const [state, dispatch] = useReducer(counterReducer, { count: 0 }); return ( <CounterContext.Provider value={{ state, dispatch }}> {children} </CounterContext.Provider> ); }

2. Using it in components

javascript
function CounterDisplay() { const { state } = useContext(CounterContext); return <p>Count: {state.count}</p>; } function CounterButtons() { const { dispatch } = useContext(CounterContext); return ( <> <button onClick={() => dispatch({ type: "decrement" })}>-</button> <button onClick={() => dispatch({ type: "increment" })}>+</button> </> ); }

3. Wrapping the app

javascript
function App() { return ( <CounterProvider> <CounterDisplay /> <CounterButtons /> </CounterProvider> ); }

What happens "under the hood"

  1. useReducer manages the local state.
  2. Context.Provider makes { state, dispatch } available globally.
  3. Any component in the tree can:
  • read the current state through useContext(Context).state;
  • change it through dispatch({ type: "..." }).

Why this is convenient

AdvantageDescription
PredictabilityAll changes are centralized through the reducer
Global availabilityThrough Context the state is available to the whole tree
ScalabilityYou can create many isolated "stores"
SimplicityNo need for Redux, Redux Toolkit, Zustand, etc.
TestabilityThe reducer is a pure function, easy to test
EncapsulationState is managed strictly through dispatch

Advanced version (splitting state and dispatch)

To avoid unnecessary re-renders, contexts are often split:

javascript
const StateContext = createContext(); const DispatchContext = createContext(); function CounterProvider({ children }) { const [state, dispatch] = useReducer(counterReducer, { count: 0 }); return ( <DispatchContext.Provider value={dispatch}> <StateContext.Provider value={state}>{children}</StateContext.Provider> </DispatchContext.Provider> ); } // now consumers of state and dispatch are independent: function useCounterState() { return useContext(StateContext); } function useCounterDispatch() { return useContext(DispatchContext); }

Now a change to state does not cause a re-render in components that are subscribed only to dispatch.


Where it's used

  • Auth: login/logout/token
  • Theme: dark/light theme
  • Cart: e-commerce cart
  • UI: modals, alerts, loaders
  • Form Wizard: transitions between steps

Summary

What it gives youHow it's implemented
Global access to stateContext
Predictable updatesuseReducer
Centralized logicreducer functions
Simple architecture without Reduxcombining Context + Reducer

Conclusion:

"Context + Reducer" is a mini-Redux built into React: simple, clean, and a perfect fit for medium-complexity applications.

Short Answer

Interview ready
Premium

A concise answer to help you respond confidently on this topic during an interview.