React state management can sometimes feel too simple or too scattered depending on the complexity of what you’re building. I remember when I was creating a form that needed to track multiple fields, validation messages, submission status, and error messages—all at once. I initially reached for useState
like I always did. But after a few updates, the logic got out of control. That’s when I finally gave useReducer
a proper chance, and everything clicked.
I had read about it before, but I always found it intimidating. The syntax reminded me of Redux, which I wasn’t in the mood to revisit. Still, I needed a clean way to manage related pieces of state, so I gave it a shot. And surprisingly, it made my code more readable, maintainable, and, honestly, a bit more satisfying to write.
When state gets complex
The moment I realized I was juggling too many useState
hooks for a single feature, I knew something had to change. For a login form, I had something like this:
const [email, setEmail] = useState('');
const [password, setPassword] = useState('');
const [error, setError] = useState(null);
const [loading, setLoading] = useState(false);
Even a small change—like resetting the form or handling an error—required updating multiple pieces of state. And the more I added, the more brittle the code felt. I started to forget what each piece was doing, and the component became bloated with state updates sprinkled all over the place.
That was the first sign that useReducer
might be a better fit.
Thinking in actions
Switching to useReducer
forced me to think in terms of actions and state transitions, not just updates. Instead of updating each value manually, I began to describe what happened:
dispatch({ type: 'LOGIN_START' });
dispatch({ type: 'LOGIN_SUCCESS', payload: user });
dispatch({ type: 'LOGIN_FAILURE', payload: error });
This model helped me centralize all logic related to state. Each action represented a meaningful event in the component’s lifecycle. And having a single reducer function to handle everything gave me a complete view of how state changes over time.
It reminded me of testing workflows I had designed before where all inputs had to be traced and debugged. When everything’s in one place, you see the flow more clearly—just like when optimizing how React renders tables or handling custom hooks.
Writing the reducer
The reducer function was the most intimidating part at first, but it turned out to be fairly logical once I stopped trying to “code it right” and just thought through the state changes step by step.
const initialState = {
email: '',
password: '',
error: null,
loading: false,
};
function reducer(state, action) {
switch (action.type) {
case 'SET_EMAIL':
return { ...state, email: action.payload };
case 'SET_PASSWORD':
return { ...state, password: action.payload };
case 'LOGIN_START':
return { ...state, loading: true, error: null };
case 'LOGIN_SUCCESS':
return { ...state, loading: false };
case 'LOGIN_FAILURE':
return { ...state, loading: false, error: action.payload };
default:
return state;
}
}
Now, I was working with just one useReducer
:
const [state, dispatch] = useReducer(reducer, initialState);
The state was easier to pass around, and changes felt like part of a story rather than a scattered mess.
Benefits I didn’t expect
One of the biggest surprises was how much easier it was to debug. Since all state changes go through the reducer, I could just log the action and state and instantly understand what went wrong. It reminded me of how server actions flow in logic when I was experimenting with useActionState—once there’s a single entry point for state updates, bugs don’t hide as easily.
Another unexpected benefit was that it became easier to write testable components. I could extract the reducer and test its logic independently without even touching React. That felt like a win in terms of long-term maintainability.
When it’s worth switching
I wouldn’t reach for useReducer
for simple state—like toggling a modal or switching a tab. But the moment I find myself creating three or more related useState
variables that change together, I now pause and think: would a reducer make this cleaner?
It’s especially helpful in forms, API calls, wizards, and components where different actions need to coordinate state updates. Even when working on performance-heavy features or building multi-step UI flows, it adds clarity and consistency.
Final thoughts
Understanding useReducer
wasn’t just about learning new syntax—it was about shifting how I structure my logic. It helped me break free from the endless chain of useState
calls and brought more organization to my components.
I still use useState
regularly. But when complexity grows, useReducer
gives me the confidence that I’m not building a house of cards. It’s not always the first tool I reach for, but I’m glad it’s there when I need it—and even happier now that I understand it.