How to Combine useContext with useReducer?
Build a Redux-like store in React by pairing useReducer with Context. Split state and dispatch contexts to avoid prop drilling and extra re-renders.
Expected Interview Answer
You combine useContext with useReducer by creating a reducer that centralizes state transitions and exposing its state and dispatch function through React Context, so any component in the tree can read state and dispatch actions without prop drilling.
useReducer manages complex state with a pure reducer function that maps the current state plus an action to the next state, while Context distributes that state and dispatch to descendants. A common refinement is two separate contexts — one for state and one for dispatch — so components that only dispatch do not re-render when state changes. This gives you a lightweight, Redux-like global store built entirely from React primitives.
- Centralized, predictable state transitions
- No prop drilling across deep component trees
- A Redux-like store without external libraries
- Testable, pure reducer logic
- Splitting contexts limits unnecessary re-renders
AI Mentor Explanation
Think of a match's third umpire as the reducer: every decision request (an action) is judged against fixed rules to produce one official outcome, and that verdict is broadcast on the big screen so every player and fan sees the same state. useContext with useReducer works alike — the reducer rules every state change and context broadcasts the result to the whole ground.
Step-by-Step Explanation
Step 1
Write the reducer
Define a pure function (state, action) => newState that handles each action type and returns the next state immutably.
Step 2
Create contexts
Create separate State and Dispatch contexts so consumers can subscribe to only what they need.
Step 3
Build a provider
In a provider component, call useReducer and pass state and dispatch to their respective context Providers.
Step 4
Expose hooks
Write custom hooks like useStore and useDispatch that read the contexts and throw if used outside the provider.
Step 5
Consume in components
Components call the hooks to read state or dispatch actions, with no props threaded through the tree.
What Interviewer Expects
- How useReducer's pure reducer manages complex state
- How Context distributes state and dispatch without prop drilling
- Why splitting state and dispatch contexts reduces re-renders
- When this pattern is preferable to plain useState or Redux
- Awareness that context updates re-render all consumers
Common Mistakes
- Mutating state inside the reducer instead of returning a new object
- Putting state and dispatch in one context, causing extra re-renders
- Recreating the context value object every render without memoization
- Using this pattern for tiny local state where useState suffices
- Forgetting to guard hooks against use outside the provider
Best Answer (HR Friendly)
“You keep all the rules for changing your app's data in one function called a reducer, then use Context to share that data and a way to trigger changes with any component that needs it. This gives you a small, organized global store without adding an outside library.”
Code Example
import { createContext, useContext, useReducer } from 'react'
const StateContext = createContext(null)
const DispatchContext = createContext(null)
const initialState = { count: 0 }
function reducer(state, action) {
switch (action.type) {
case 'increment':
return { ...state, count: state.count + 1 }
case 'decrement':
return { ...state, count: state.count - 1 }
case 'reset':
return { ...state, count: 0 }
default:
throw new Error('Unknown action: ' + action.type)
}
}
export function StoreProvider({ children }) {
const [state, dispatch] = useReducer(reducer, initialState)
return (
<StateContext.Provider value={state}>
<DispatchContext.Provider value={dispatch}>
{children}
</DispatchContext.Provider>
</StateContext.Provider>
)
}
export function useStore() {
const ctx = useContext(StateContext)
if (ctx === null) throw new Error('useStore must be used within StoreProvider')
return ctx
}
export function useDispatch() {
const ctx = useContext(DispatchContext)
if (ctx === null) throw new Error('useDispatch must be used within StoreProvider')
return ctx
}
export function Counter() {
const { count } = useStore()
const dispatch = useDispatch()
return (
<div>
<span>{count}</span>
<button onClick={() => dispatch({ type: 'increment' })}>+</button>
<button onClick={() => dispatch({ type: 'decrement' })}>-</button>
</div>
)
}Follow-up Questions
- Why split state and dispatch into two separate contexts?
- How does this pattern compare to Redux or Zustand?
- How would you handle async actions with this setup?
- Why must the reducer be a pure function?
- How do you prevent all consumers re-rendering on every state change?
MCQ Practice
1. What is the signature of a reducer function?
A reducer takes the current state and an action and returns the next state, without mutating the original.
2. Why split state and dispatch into two contexts?
Since dispatch is stable, components that only dispatch avoid re-rendering when the state context value changes.
3. What must a reducer avoid doing?
Reducers must be pure and return new state; mutating existing state breaks React's change detection and predictability.
Flash Cards
What does useReducer provide? — A state value and a dispatch function driven by a pure (state, action) => newState reducer.
Role of Context in this pattern? — It distributes the reducer's state and dispatch to any descendant without prop drilling.
Why two contexts (state and dispatch)? — Dispatch is stable, so dispatch-only consumers avoid re-rendering when state changes.
Key rule for the reducer? — It must be pure: return new state, never mutate the current state.