Welcome back to the React Mastery Series!

In the previous article, we explored the Context API and learned how to eliminate prop drilling by sharing data across multiple components.

However, as applications become more complex, managing state using multiple useState() Hooks can quickly become difficult.

Imagine building:

  • An online banking dashboard
  • A shopping cart
  • A task management application
  • A multi-step registration form

Each of these applications may involve dozens of related state updates.

React provides another Hook specifically designed for handling complex state logic useReducer


Why Do We Need useReducer?

Let's first understand the problem.

Suppose you're building a shopping cart.

Using useState, you might write:

const [items, setItems] = useState([]);

const [total, setTotal] = useState(0);

const [discount, setDiscount] = useState(0);

const [tax, setTax] = useState(0);

const [shipping, setShipping] = useState(100);

Enter fullscreen mode Exit fullscreen mode

Now imagine every action updates multiple states.

Adding a product:

  • Updates items
  • Recalculates total
  • Recalculates tax
  • Applies discounts
  • Updates shipping

Managing all this logic separately becomes difficult.


What is useReducer?

useReducer is a Hook used to manage complex state transitions.

Instead of updating state directly, we:

  • Dispatch an action
  • Let a reducer decide how state should change

Think of it like this:

User Action
      |
      ↓
Dispatch Action
      |
      ↓
Reducer Function
      |
      ↓
New State
      |
      ↓
Component Re-renders

Enter fullscreen mode Exit fullscreen mode

The reducer becomes the central place where all state updates happen.


Basic Syntax

const [state, dispatch] = useReducer(reducer, initialState);

Enter fullscreen mode Exit fullscreen mode

Parameters:

  • reducer → Function that updates the state
  • initialState → Starting value

Returns:

  • state
  • dispatch

Understanding the Reducer Function

The reducer receives:

(state, action)

Enter fullscreen mode Exit fullscreen mode

Example:

function counterReducer(state, action) {

  switch (action.type) {

    case "increment":
      return {
        count: state.count + 1
      };

    case "decrement":
      return {
        count: state.count - 1
      };

    default:
      return state;

  }

}

Enter fullscreen mode Exit fullscreen mode

A reducer always:

  • Receives current state
  • Receives an action
  • Returns new state

It should never mutate the existing state.


Initial State

const initialState = {
  count: 0
};

Enter fullscreen mode Exit fullscreen mode

React uses this value during the first render.


Creating the Reducer

const [state, dispatch] = useReducer( counterReducer, initialState);

Enter fullscreen mode Exit fullscreen mode

Now we can access:

state.count

Enter fullscreen mode Exit fullscreen mode


Dispatching Actions

Instead of:

setCount(count + 1);

Enter fullscreen mode Exit fullscreen mode

we dispatch an action.

Example:

dispatch({type: "increment"});

Enter fullscreen mode Exit fullscreen mode

Flow:

Button Click
      |
      ↓
dispatch()
      |
      ↓
Reducer Executes
      |
      ↓
Returns New State
      |
      ↓
React Re-renders

Enter fullscreen mode Exit fullscreen mode


Complete Counter Example

import { useReducer } from "react";

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,
      };

    default:
      return state;
  }
}

function Counter() {
  const [state, dispatch] = useReducer(reducer, initialState);

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

      <button onClick={() => dispatch({ type: "increment" })}>+</button>

      <button onClick={() => dispatch({ type: "decrement" })}>-</button>
    </div>
  );
}

Enter fullscreen mode Exit fullscreen mode


Understanding Actions

Actions are plain JavaScript objects.

Example:

{
  type: "increment"
}

Enter fullscreen mode Exit fullscreen mode

Actions can also carry additional data.

Example:

{
  type: "deposit",
  amount: 5000
}

Enter fullscreen mode Exit fullscreen mode

The reducer decides what to do with that information.


Passing Data with Payload

Example:

dispatch({ type: "deposit", amount: 5000});

Enter fullscreen mode Exit fullscreen mode

Reducer:

case "deposit":

  return {balance: state.balance + action.amount};

Enter fullscreen mode Exit fullscreen mode

This pattern is widely used in enterprise applications.


Real-World Example: Bank Account

Initial state:

const initialState = {
  balance: 10000
};

Enter fullscreen mode Exit fullscreen mode

Reducer:

function reducer(state, action) {
  switch (action.type) {
    case "deposit":
        return {balance: state.balance + action.amount};

    case "withdraw":
        return {balance: state.balance - action.amount};

    default:
      return state;
  }
}

Enter fullscreen mode Exit fullscreen mode

Flow:

Deposit Button
      |
      ↓
Dispatch Action
      |
      ↓
Reducer Updates Balance
      |
      ↓
UI Refreshes

Enter fullscreen mode Exit fullscreen mode


Why Not Just Use useState?

Consider a login form.

Using multiple states:

const [email, setEmail] = useState("");

const [password, setPassword] = useState("");

const [loading, setLoading] = useState(false);

const [error, setError] = useState("");

const [user, setUser] = useState(null);

Enter fullscreen mode Exit fullscreen mode

As the application grows, related state becomes harder to manage.

With useReducer:

const initialState = {
  email: "",
  password: "",
  loading: false,
  error: "",
  user: null
};

Enter fullscreen mode Exit fullscreen mode

Everything is managed in one predictable place.


Combining Context with useReducer

One of the most common enterprise patterns is:

Context API
      |
      ↓
useReducer
      |
      ↓
Global State

Enter fullscreen mode Exit fullscreen mode

Example:

<AuthContext.Provider
  value={{
    state,
    dispatch,
  }}
>
  <App />
</AuthContext.Provider>;

Enter fullscreen mode Exit fullscreen mode

Now every component can dispatch actions.


Action Flow in Enterprise Applications

User Clicks Login
         |
         ↓
Dispatch LOGIN_REQUEST
         |
         ↓
Reducer Updates Loading State
         |
         ↓
API Call
         |
         ↓
Dispatch LOGIN_SUCCESS
         |
         ↓
Reducer Stores User
         |
         ↓
Dashboard Updates

Enter fullscreen mode Exit fullscreen mode

This architecture is very similar to Redux.


Common Action Types

Examples:

LOGIN_REQUEST

LOGIN_SUCCESS

LOGIN_FAILURE

FETCH_USERS

ADD_PRODUCT

DELETE_PRODUCT

UPDATE_PROFILE

LOGOUT

Enter fullscreen mode Exit fullscreen mode

Action names should clearly describe what happened.


Common Mistakes

1. Mutating State

Incorrect:

state.count++;

return state;

Enter fullscreen mode Exit fullscreen mode

Correct:

return {
  count: state.count + 1
};

Enter fullscreen mode Exit fullscreen mode

Always return a new object.


2. Too Much Logic Inside Components

Avoid:

if(user.role === "ADMIN"){

// update many states

}

Enter fullscreen mode Exit fullscreen mode

Move business logic into the reducer whenever possible.


3. Missing Default Case

Always include:

default:
  return state;

Enter fullscreen mode Exit fullscreen mode

This ensures unknown actions don't break your application.


useState vs useReducer

useState useReducer
Simple state Complex state
Easy to learn Better for large logic
Direct updates Action-based updates
Few state variables Multiple related state values
Best for local UI Best for predictable state transitions

Enterprise Example: Retail Banking Dashboard

Imagine a customer dashboard.

Available actions:

Load Accounts

Load Transactions

Transfer Money

Update Profile

Logout

Enter fullscreen mode Exit fullscreen mode

Flow:

User Action
      |
      ↓
Dispatch Action
      |
      ↓
Reducer
      |
      ↓
Update Global State
      |
      ↓
React Re-renders UI

Enter fullscreen mode Exit fullscreen mode

Every state change follows the same predictable pattern.


Best Practices

  • Use useState for simple state.
  • Use useReducer for complex or related state.
  • Keep reducers pure.
  • Never mutate state directly.
  • Use meaningful action names.
  • Separate reducer logic from UI components.
  • Combine useReducer with Context for scalable applications.

Key Takeaways

Today, we learned:

useReducer manages complex state transitions.
✅ Reducers receive the current state and an action.
✅ Actions describe what happened, while reducers decide how state changes.
dispatch() sends actions to the reducer.
useReducer works exceptionally well with the Context API.
✅ The architecture is similar to Redux, making it easier to learn Redux later.


Coming Next 🚀

In Day 18, we will explore:

Custom Hooks in React – Reusing Logic Like a Pro

We will learn:

  • What Custom Hooks are
  • Why they are useful
  • Creating reusable Hooks
  • Sharing logic across components
  • Real-world examples
  • Best practices and common mistakes

Custom Hooks are one of the most powerful features of React and are widely used in enterprise applications to keep code clean, reusable, and maintainable.

Happy Coding! 🚀