
Reducer Patterns with useReducer
useReducer is a state management hook for complex state logic — when next state depends on previous state across multiple operations, or when updates benefit from explicit named transitions.
Basic Usage
const initialState = { count: 0 };
function reducer(state, action) {
switch (action.type) {
case 'increment':
return { count: state.count + 1 };
case 'decrement':
return { count: state.count - 1 };
case 'reset':
return initialState;
default:
throw new Error(`Unknown action: ${action.type}`);
}
}
function Counter() {
const [state, dispatch] = useReducer(reducer, initialState);
return (
<>
<p>{state.count}</p>
<button onClick={() => dispatch({ type: 'increment' })}>+</button>
<button onClick={() => dispatch({ type: 'decrement' })}>-</button>
</>
);
} When useReducer Over useState
- Multiple related state fields that update together
- Next state depends on previous state in complex ways
- State transitions have names that make the code self-documenting
- You want to test state logic independently of the component
// useState version — hard to reason about
const [loading, setLoading] = useState(false);
const [data, setData] = useState(null);
const [error, setError] = useState(null);
// useReducer version — explicit, consistent transitions
const [state, dispatch] = useReducer(fetchReducer, {
status: 'idle', data: null, error: null,
});
// state.status is always 'idle' | 'loading' | 'success' | 'error' Action Creators
const actions = {
increment: () => ({ type: 'increment' }),
addItem: (item) => ({ type: 'add_item', payload: item }),
};
dispatch(actions.addItem(newItem)); Immer Integration
import produce from 'immer';
const reducer = produce((draft, action) => {
switch (action.type) {
case 'add_item':
draft.items.push(action.payload);
break;
case 'remove_item':
draft.items = draft.items.filter(i => i.id !== action.payload);
break;
}
}); Lazy Initialization
const [state, dispatch] = useReducer(reducer, initialArg, init);
// init(initialArg) called once — defers expensive computation Sharing Dispatch via Context
const DispatchContext = createContext();
function AppProvider({ children }) {
const [state, dispatch] = useReducer(reducer, initialState);
return (
<DispatchContext.Provider value={dispatch}>
<StateContext.Provider value={state}>
{children}
</StateContext.Provider>
</DispatchContext.Provider>
);
}
// Anywhere in the tree
const dispatch = useContext(DispatchContext);
dispatch({ type: 'update_user', payload: user }); dispatch is stable — components that only dispatch don’t re-render when state changes.
Testing Reducers
Reducers are pure functions — trivial to unit test:
test('increment increases count', () => {
const state = reducer({ count: 5 }, { type: 'increment' });
expect(state.count).toBe(6);
}); 








