Mahamudul Hasan Rubel
HomeAboutProjectsSkillsExperienceBlogPhotosContact
Mahamudul Hasan Rubel

Senior Software Engineer crafting high-performance web applications and SaaS platforms.

Navigation

  • Home
  • About
  • Projects
  • Skills
  • Experience
  • Blog
  • Photos
  • Contact

Get in Touch

Available for senior/lead roles and consulting.

bd.mhrubel@gmail.comHire Me

© 2026 Mahamudul Hasan Rubel. All rights reserved.

Built with using Next.js 16 & Tailwind v4

Back to Blog
ReactNext.jsJune 24, 20264 min read

React state management: Reducers vs. State Machines

React state management can get messy with nested booleans. Learn why switching from useReducer to state machines helps you avoid impossible UI states.

ReactJavaScriptState ManagementFrontend ArchitectureXStateWeb DevelopmentNext.jsTutorial

I remember sitting at my desk at 2:00 AM, staring at a login form that somehow managed to show an "Error" message while simultaneously displaying a "Loading" spinner. It was a classic case of tangled boolean flags, and it’s a rite of passage for every frontend dev. If you're tired of debugging race conditions where your component thinks it's both idle and submitting, it’s time to rethink your approach to React state management.

When your UI logic starts feeling like a house of cards, you usually reach for useReducer. It’s a massive step up from useState because it centralizes your logic. But even with reducers, you can still find yourself in "impossible states" if you aren't careful.

Why useReducer isn't always enough

A reducer is just a function that takes the current state and an action to return a new state. It’s great for predictability, but it doesn't inherently prevent you from transitioning to an invalid state.

Take a file upload component. You might have isUploading, isError, and isSuccess flags. If you aren't disciplined, you can easily trigger a sequence where isUploading and isError are both true. You’ve built a state machine in your head, but the code doesn't enforce it.

Before we dive deeper, it helps to understand how React state snapshots: A mental model for functional components work. Every render captures a specific version of your state. If your logic relies on multiple independent variables, that snapshot can quickly become inconsistent.

The shift to state machines

This is where state machines change the game. Instead of managing independent booleans, you define explicit states: IDLE, UPLOADING, SUCCESS, and ERROR. You then define the allowed transitions between them.

A state machine forces you to be intentional. You can’t go from SUCCESS back to UPLOADING unless you explicitly define that transition. This eliminates entire classes of bugs. When I first started using them, I felt like I was writing more boilerplate, but I spent about 30% less time on bug fixes during the following sprint.

If you're using TypeScript, you can leverage TypeScript state machines: Building predictable UI logic with XState to make this transition even safer. It turns your logic into a strict contract that the compiler enforces.

Comparing the two approaches

Let’s look at a simplified example of how we handle a fetch request.

The Reducer Approach:

JAVASCRIPT
function reducer(state, action) {
  switch (action.type) {
    case CE9178">'FETCH_START':
      return { ...state, loading: true, error: null };
    case CE9178">'FETCH_SUCCESS':
      return { ...state, loading: false, data: action.payload };
    case CE9178">'FETCH_ERROR':
      return { ...state, loading: false, error: action.payload };
    default:
      return state;
  }
}

The State Machine Approach:

JAVASCRIPT
const machine = {
  initial: CE9178">'idle',
  states: {
    idle: { on: { FETCH: CE9178">'loading' } },
    loading: { on: { SUCCESS: CE9178">'success', ERROR: CE9178">'error' } },
    success: { on: { FETCH: CE9178">'loading' } },
    error: { on: { FETCH: CE9178">'loading' } }
  }
};

In the reducer, you have to manually reset error to null every time you start a fetch. In the state machine, moving to the loading state implicitly clears your previous context. You stop managing flags and start managing behavior.

When should you switch?

Don't go overboard. If your component is a simple counter or a toggle, stick with useState. If you have a form with one or two fields, useReducer is fine.

However, if you find yourself writing useEffect hooks that track three or more variables to determine if a button should be disabled, you’ve outgrown simple hooks. That’s the moment to model your component as a state machine. It’s not about complexity; it’s about clarity.

Remember that React state management: Mapping your Next.js component hierarchy is still the foundation. Before you reach for a state machine library, make sure your state is living as close to the relevant components as possible.

Frequently Asked Questions

Does using state machines make my bundle size huge? It depends on the library. XState is powerful but can be heavy. For smaller projects, you can write a "mini-machine" using a simple object map and useReducer without adding a heavy dependency.

Is it overkill for simple forms? Usually, yes. Don't let the "state machine" label intimidate you into over-engineering. If your form logic is straightforward, keep it simple.

Can I mix these patterns? Absolutely. You don't have to refactor your entire app. I often use standard React hooks for local UI state and reserve state machines for complex workflow orchestrations like multi-step wizards or authentication flows.

I’m still experimenting with how much logic I should pull out of the UI layer. Sometimes, moving too much logic into a machine makes it harder for the next developer to see what’s happening at a glance. My advice? Start by identifying the "impossible states" you currently face, and use a machine only when those bugs become a recurring cost.

Back to Blog

Similar Posts

ReactNext.jsJune 24, 20264 min read

React component communication: Mastering Callback Patterns and Props

Master React component communication by moving beyond basic props. Learn how to use callback functions to handle data flow and simplify your state logic.

Read more
ReactNext.js
June 24, 2026
3 min read

React form handling: Controlled vs. Uncontrolled Components

React form handling doesn't have to be complex. Learn the trade-offs between controlled and uncontrolled components to decide when to sync your UI state.

Read more
ReactNext.jsJune 24, 20264 min read

React State Management: Mastering Component Initialization and Syncing

React state management during component initialization often leads to bugs. Learn how to handle prop-to-state syncing correctly using standard React patterns.

Read more