# useReducer in React: A Simple Counter, Then the Bigger Picture

## Why Does `useReducer` Even Exist?

Before writing a single line of code, let's build the right mental model.

You already know `useState`. It's clean, it's simple, and it works great when a piece of state is self-contained — like a toggle, a string input, or a counter. But as your component grows, you'll notice something uncomfortable: the *logic* for how state changes starts spreading everywhere. One button calls `setCount(count + 1)`, another calls `setCount(0)`, another calls `setCount(count - 1)`, and suddenly your JSX is littered with little inline decisions about *how* state should change.

`useReducer` says: *<mark class="bg-yellow-200 dark:bg-yellow-500/30">what if all those decisions lived in one place?</mark>*

That one place is called a **reducer** — a pure function (returns same results with same set of inputs) that takes your current state and an action, and returns the new state. That's the whole pattern. Everything else is just details.

* * *

## The Counter Example

Let's start with the simplest possible example: a counter with increment, decrement, and reset.

### Step 1: Define Your State and Actions

First, think about what your state looks like and what can happen to it. For a counter, the state is just a number. The things that can happen are: go up, go down, reset.

```javascript
// This is what our state looks like
const initialState = { count: 0 };

// These are the "things that can happen" — we call them actions.
// Each action is just a plain object with a 'type' field.
// { type: 'INCREMENT' }
// { type: 'DECREMENT' }
// { type: 'RESET' }
```

### Step 2: Write the Reducer

The reducer is a function with a simple contract: given the *current state* and an *action*, return the *next state*. <mark class="bg-yellow-200 dark:bg-yellow-500/30">It never mutates — it always returns a fresh value.</mark>

```js
function counterReducer(state, action) {
  switch (action.type) {
    case 'INCREMENT':
      return { count: state.count + 1 };

    case 'DECREMENT':
      return { count: state.count - 1 };

    case 'RESET':
      return { count: 0 };

    default:
      // If we receive an unknown action, return state unchanged.
      // This is a safety net — never throw away state accidentally.
      return state;
  }
}
```

Notice what's happening here: the reducer is a *map* from (state, action) → new state. It has no side effects. It doesn't call any APIs, doesn't touch the DOM, and doesn't depend on anything outside its arguments. This purity is what makes reducers so predictable and easy to test.

### Step 3: Wire It Into the Component

Now you use `useReducer(reducer, initialState)`. It hands you back two things — the current state, and a `dispatch` function. When you call `dispatch({ type: 'INCREMENT' })`, React runs your reducer with that action and re-renders the component with the new state.

```jsx
import { useReducer } from 'react';

function counterReducer(state, action) {
  switch (action.type) {
    case 'INCREMENT':
      return { count: state.count + 1 };
    case 'DECREMENT':
      return { count: state.count - 1 };
    case 'RESET':
      return { count: 0 };
    default:
      return state;
  }
}

const initialState = { count: 0 };

export default function Counter() {
  // useReducer returns [currentState, dispatchFunction]
  const [state, dispatch] = useReducer(counterReducer, initialState);

  return (
    <div>
      <h2>Count: {state.count}</h2>

      {/* Each button just dispatches an action — it doesn't know HOW state changes */}
      <button onClick={() => dispatch({ type: 'INCREMENT' })}>+</button>
      <button onClick={() => dispatch({ type: 'DECREMENT' })}>−</button>
      <button onClick={() => dispatch({ type: 'RESET' })}>Reset</button>
    </div>
  );
}
```

Take a moment to notice how the JSX is now clean. The buttons don't contain logic — they just declare *intent*. "I want to increment." The reducer decides what that means.

* * *

## The Key Mental Shift: Events vs. Mutations

Here's the most important idea behind the reducer pattern, and it's worth sitting with.

With `useState`, you think like this: *"When this button is clicked, set the count to count + 1."* You're thinking about **mutations** — directly manipulating the value.

With `useReducer`, you think like this: *"When this button is clicked, something happened — an INCREMENT event occurred."* You're thinking about **events** (or actions), and you're letting the reducer decide what the state should look like in response to that event.

This is the same shift that makes Redux, Flux, and even event-sourced databases so powerful. Your component becomes a *sender of events*, not a *manager of state*. The reducer owns all the logic.

* * *

## Actions with Payloads

Real-world actions often carry data. Suppose you want an "add by N" button. You'd pass the number as a payload inside the action object:

```js
// Dispatching an action with a payload
dispatch({ type: 'ADD_BY', payload: 5 });

// And in the reducer...
case 'ADD_BY':
  return { count: state.count + action.payload };
```

The convention is to use `type` for the action name and `payload` for the data it carries. This isn't enforced by React — it's just a widely adopted community pattern that keeps things readable.

* * *

## When to Prefer `useReducer` Over `useState`

The React docs themselves give a clear rule of thumb here, and it maps well to real experience.

Reach for `useReducer` when your state has **multiple sub-values** that change together (like a form with name, email, and password), when **the next state depends on the previous state** in complex ways, or when you have **many different ways to update state** and the component is getting hard to read. If you ever find yourself writing three or four `setState` calls in a single event handler to keep things in sync, that's a strong signal to consolidate into a reducer.

On the other hand, `useState` remains the right tool for simple, isolated values. A modal's `isOpen` flag doesn't need a reducer. A single text input doesn't need a reducer. Match the tool to the complexity.

* * *

## A Slightly Richer Example: Form State

To cement the idea, here's how the same reducer pattern scales gracefully to a login form — the kind of thing that would get messy fast with three separate `useState` calls.

```jsx
const initialState = {
  username: '',
  password: '',
  isSubmitting: false,
};

function formReducer(state, action) {
  switch (action.type) {
    case 'SET_FIELD':
      // action.payload = { field: 'username', value: 'yashraj' }
      return { ...state, [action.payload.field]: action.payload.value };

    case 'SUBMIT_START':
      return { ...state, isSubmitting: true };

    case 'SUBMIT_DONE':
      return { ...state, isSubmitting: false };

    default:
      return state;
  }
}

function LoginForm() {
  const [state, dispatch] = useReducer(formReducer, initialState);

  const handleChange = (e) => {
    dispatch({
      type: 'SET_FIELD',
      payload: { field: e.target.name, value: e.target.value },
    });
  };

  const handleSubmit = async () => {
    dispatch({ type: 'SUBMIT_START' });
    await fakeApiCall(state);
    dispatch({ type: 'SUBMIT_DONE' });
  };

  return (
    <div>
      <input name="username" value={state.username} onChange={handleChange} />
      <input name="password" type="password" value={state.password} onChange={handleChange} />
      <button onClick={handleSubmit} disabled={state.isSubmitting}>
        {state.isSubmitting ? 'Logging in...' : 'Login'}
      </button>
    </div>
  );
}
```

Notice that `SET_FIELD` handles *all* fields with a single case — because the field name is part of the payload. All three fields stay in one state object, transitions are all visible in one reducer, and the component is left responsible only for rendering and dispatching.

* * *

## The Bigger Picture: Where This Pattern Lives

`useReducer` is React's take on a much older idea. The reducer pattern is borrowed from functional programming — specifically from the `Array.prototype.reduce` idea that you fold a list of changes into a final value. Redux, which became enormously popular in the React ecosystem, is essentially this pattern at the application level.

Understanding `useReducer` well also makes the leap to Redux Context patterns, Zustand, and other state libraries much easier — because they all share this same vocabulary of *actions*, *reducers*, and *dispatch*.

* * *

## Notes

The reducer pattern is fundamentally about **separating what happened from what it means**. Your UI dispatches events. Your reducer decides what those events mean for state. That separation keeps your components clean, your logic testable, and your state transitions auditable — you can read the reducer top to bottom and know every possible way your state can change.

Start with the counter. Internalize the `(state, action) => newState` contract. Then notice how naturally it scales when your state grows more complex. That's the pattern working as designed.

<script type="application/ld+json">
[
  {
    "@context": "https://schema.org",
    "@type": "TechArticle",
    "headline": "Taming Form State with useReducer: From Messy to Production-Ready",
    "description": "Learn how to refactor messy React form logic into a clean, scalable, and production-ready solution using the useReducer hook.",
    "author": {
      "@type": "Person",
      "name": "Yashraj",
      "url": "https://yashrajxdev.blog"
    },
    "publisher": {
      "@type": "Organization",
      "name": "yashrajxdev.blog"
    },
    "mainEntityOfPage": {
      "@type": "WebPage",
      "@id": "https://yashrajxdev.blog/taming-form-state-with-usereducer-from-messy-to-production-ready"
    },
    "keywords": "React, useReducer, Form State Management, Frontend Engineering, JavaScript, Web Development",
    "proficiencyLevel": "Intermediate",
    "dependencies": "React 16.8+"
  },
  {
    "@context": "https://schema.org",
    "@type": "HowTo",
    "name": "How to Refactor React Forms with useReducer",
    "description": "A step-by-step guide to moving from multiple useState calls to a centralized useReducer pattern for complex forms.",
    "step": [
      {
        "@type": "HowToStep",
        "name": "Define Initial State",
        "text": "Identify all form fields and create a central state object.",
        "url": "https://yashrajxdev.blog/taming-form-state-with-usereducer-from-messy-to-production-ready#step1"
      },
      {
        "@type": "HowToStep",
        "name": "Create the Reducer Function",
        "text": "Write a pure function that handles action types like UPDATE_FIELD and RESET_FORM.",
        "url": "https://yashrajxdev.blog/taming-form-state-with-usereducer-from-messy-to-production-ready#step2"
      },
      {
        "@type": "HowToStep",
        "name": "Implement Dispatch logic",
        "text": "Connect your input change handlers to the dispatch function to update the central state.",
        "url": "https://yashrajxdev.blog/taming-form-state-with-usereducer-from-messy-to-production-ready#step3"
      }
    ]
  },
  {
    "@context": "https://schema.org",
    "@type": "SoftwareSourceCode",
    "name": "React useReducer Form Example",
    "description": "Clean implementation of a multi-field form using React useReducer.",
    "programmingLanguage": "JavaScript",
    "runtimePlatform": "React",
    "codeSampleType": "Snippet"
  },
  {
    "@context": "https://schema.org",
    "@type": "BreadcrumbList",
    "itemListElement": [
      {
        "@type": "ListItem",
        "position": 1,
        "name": "Home",
        "item": "https://yashrajxdev.blog"
      },
      {
        "@type": "ListItem",
        "position": 2,
        "name": "Blogs",
        "item": "https://yashrajxdev.blog/blogs"
      },
      {
        "@type": "ListItem",
        "position": 3,
        "name": "Taming Form State with useReducer",
        "item": "https://yashrajxdev.blog/taming-form-state-with-usereducer-from-messy-to-production-ready"
      }
    ]
  }
]
</script>
