Dev.to WebDev 🛠 Dev 👁 0 📖 4 min read

React Mastery Series – Day 17: Advanced State Management – Mastering useReducer

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

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);

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

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

Basic Syntax

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

Parameters:

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

Returns:

  • state
  • dispatch

Understanding the Reducer Function

The reducer receives:

(state, action)

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;

  }

}

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

React uses this value during the first render.

Creating the Reducer

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

Now we can access:

state.count

Dispatching Actions

Instead of:

setCount(count + 1);

we dispatch an action.

Example:

dispatch({type: "increment"});

Flow:

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

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

Understanding Actions

Actions are plain JavaScript objects.

Example:

{
  type: "increment"
}

Actions can also carry additional data.

Example:

{
  type: "deposit",
  amount: 5000
}

The reducer decides what to do with that information.

Passing Data with Payload

Example:

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

Reducer:

case "deposit":

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

This pattern is widely used in enterprise applications.

Real-World Example: Bank Account

Initial state:

const initialState = {
  balance: 10000
};

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

Flow:

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

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);

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

With useReducer:

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

Everything is managed in one predictable place.

Combining Context with useReducer

One of the most common enterprise patterns is:

Context API
      |
      ↓
useReducer
      |
      ↓
Global State

Example:

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

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

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

Action names should clearly describe what happened.

Common Mistakes

1. Mutating State

Incorrect:

state.count++;

return state;

Correct:

return {
  count: state.count + 1
};

Always return a new object.

2. Too Much Logic Inside Components

Avoid:

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

// update many states

}

Move business logic into the reducer whenever possible.

3. Missing Default Case

Always include:

default:
  return state;

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

Flow:

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

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! 🚀

📰 Read the original article on Dev.to WebDev

Originally published by Dev.to WebDev. Aggregated on AIWithGhost for educational purposes — full credit and traffic to the original publisher.