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"
useReducermanages the local state.Context.Providermakes{ state, dispatch }available globally.- Any component in the tree can:
- read the current state through
useContext(Context).state; - change it through
dispatch({ type: "..." }).
Why this is convenient
| Advantage | Description |
|---|---|
| Predictability | All changes are centralized through the reducer |
| Global availability | Through Context the state is available to the whole tree |
| Scalability | You can create many isolated "stores" |
| Simplicity | No need for Redux, Redux Toolkit, Zustand, etc. |
| Testability | The reducer is a pure function, easy to test |
| Encapsulation | State 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 you | How it's implemented |
|---|---|
| Global access to state | Context |
| Predictable updates | useReducer |
| Centralized logic | reducer functions |
| Simple architecture without Redux | combining 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 readyPremium
A concise answer to help you respond confidently on this topic during an interview.