Jihye's Blog

image for usestate-react

When Simplicity Wins

Choosing useState Over useReducer in React

Photo by Jakub Dziubak

In the past, every time I created Context in React apps, I found myself managing complex states. Naturally, using the useReducer hook became my go-to solution. My typical implementation involved setting up useReducer with separate reducers for handling API data and state values. This pattern gave me control and structure.

Recently, while building a new Context in a fresh project, I instinctively reached for useReducer. But very soon, I realised something: it was overkill. The state I needed to manage was a simple string value, updated occasionally. There was no need for complex transitions or multiple action types. Using useState would be far simpler and more appropriate.

This made me think. Even though I used to prefer simple solutions—even if they took more effort—I had defaulted to a more complex approach. Maybe because I had more experience now, and I thought using advanced tools showed that. But being a senior engineer also means knowing when not to use those tools.

The Temptation of Overengineering

My excuse for using useReducer was future-proofing: "The state might get more complicated later." But the reality was that it wasn’t complicated now. And that matters. Writing code for future complexity that may never come adds unnecessary weight and reduces clarity.

Here’s what I learned and applied:

When to Use useState

  • Your logic is straightforward.
  • You're managing simple, isolated pieces of state.
  • You don't have multiple action types.
  • State transitions are easy to understand and update.

When to Use useReducer

  • Your state has complex transitions.
  • You need to handle multiple action types.
  • The next state depends on the previous state in nuanced ways.
  • You want to group related updates atomically.

The reducer pattern is powerful, but it also introduces complexity:

  • Action type constants
  • Reducer functions
  • Dispatch calls
  • Action objects with type and payload

If your use case doesn't demand that structure, you're better off keeping things simple.

Benefits of Switching to useState

  • Less boilerplate: Fewer lines of code and cleaner syntax
  • Improved readability: Easier for others (and future you) to understand
  • Better performance: For simple state updates, useState is slightly more performant since it skips the reducer flow

Final Thoughts

This experience reminded me that good engineering isn't about using the most advanced tools—it's about using the right ones. Sometimes, the simplest solution is also the best one. As engineers grow, it's easy to reach for more powerful patterns out of habit or perceived best practices. But experience also teaches us when to pull back.

In the end, simplicity won. And my codebase (and my future self) is better for it.