Lesson 4 — useReducer Hook (Advanced State Logic)

🧭 Introduction

As your React applications grow, state management often becomes complex:

  • Multiple related state values
  • Conditional updates
  • Repeated state logic
  • Difficult-to-read useState chains

This is where useReducer shines.

Many developers think useReducer is “just Redux lite” — but that’s not the right mental model.

In this lesson, you’ll learn:

  • Why useReducer exists
  • When to use it instead of useState
  • How it improves readability, predictability, and scalability

🎯 What You’ll Learn in This Lesson

By the end of this lesson, you will understand:

  • The problem with complex useState logic
  • What useReducer actually does
  • Reducer pattern fundamentals
  • Action-based state updates
  • Real-world useReducer example
  • Best practices and common mistakes

❓ Why useState Breaks Down for Complex Logic

useState works great for simple, independent state.

Simple useState (Perfect)

const [count, setCount] = useState(0);

But look at this 👇

❌ Complex useState Example

const [count, setCount] = useState(0);
const [loading, setLoading] = useState(false);
const [error, setError] = useState(null);

Now add conditions:

  • Reset error on success
  • Prevent update while loading
  • Handle multiple actions

👉 Logic becomes scattered and fragile.


🧠 What is useReducer? (Clear Definition)

useReducer is:

A hook that manages state using a reducer function and actions.

Instead of saying:

“Set this value”

You say:

“This action happened — update state accordingly”

This leads to:
✅ Predictable updates
✅ Centralized logic
✅ Easier debugging


🧩 useReducer Syntax

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

Where:

  • state → current state
  • dispatch → sends actions
  • reducer → decides how state changes
  • initialState → starting state

🔁 Reducer Function Explained

A reducer is a pure function:

function reducer(state, action) {
  switch (action.type) {
    case "INCREMENT":
      return { count: state.count + 1 };

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

    default:
      return state;
  }
}

Key Rules of Reducers

✅ Must be pure
✅ No side effects
✅ Must return new state
❌ Must not mutate state


🧪 Simple useReducer 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 (
    <>
      <h2>{state.count}</h2>
      <button onClick={() => dispatch({ type: "INCREMENT" })}>+</button>
      <button onClick={() => dispatch({ type: "DECREMENT" })}>-</button>
    </>
  );
}

👉 State updates are explicit and predictable.


🧠 Why useReducer Scales Better

Compare mental models:

useState

setCount(count + 1);

❓ Why did state change?
❓ What triggered it?

useReducer

dispatch({ type: "INCREMENT" });

✅ Clear intent
✅ Action-driven updates

This is extremely useful for:

  • Forms
  • Wizards
  • Complex UI flows
  • Async state handling

🔍 Real-World Example — Form State

Initial State

const initialState = {
  value: "",
  error: null,
  touched: false
};

Reducer

function reducer(state, action) {
  switch (action.type) {
    case "CHANGE":
      return {
        ...state,
        value: action.payload,
        error: null
      };

    case "BLUR":
      return {
        ...state,
        touched: true
      };

    case "ERROR":
      return {
        ...state,
        error: action.payload
      };

    default:
      return state;
  }
}

Component

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

<input
  value={state.value}
  onChange={(e) =>
    dispatch({ type: "CHANGE", payload: e.target.value })
  }
  onBlur={() => dispatch({ type: "BLUR" })}
/>

👉 All form logic lives in one place.


⚠ Common Mistakes with useReducer

❌ Mistake 1: Using useReducer for Everything

If state is simple → useState is better.


❌ Mistake 2: Putting Side Effects in Reducer

// ❌ WRONG
case "FETCH":
  fetch("/api");

Reducers must stay pure.


❌ Mistake 3: Overcomplicated Action Types

Keep actions meaningful and readable.


🧠 useReducer vs useState (Quick Comparison)

ScenarioRecommended
Simple toggleuseState
Independent valuesuseState
Related state logicuseReducer
Complex transitionsuseReducer
Predictable updatesuseReducer

🔗 useReducer + useContext (Preview)

In advanced apps:

  • useReducer handles logic
  • useContext shares state

👉 This pattern scales beautifully
(We’ll cover this later.)


🎯 Best Practices

✅ Keep reducer logic centralized
✅ Use clear action names
✅ Avoid side effects in reducer
✅ Prefer readability over cleverness


❓ FAQs — useReducer Hook

🔹 Is useReducer better than Redux?

No. It solves local component state, not global state.


🔹 Can useReducer replace useState?

Only when logic becomes complex.


🔹 Does useReducer improve performance?

Not automatically — it improves clarity and predictability.


🔹 Is useReducer mandatory for advanced apps?

No, but knowing when to use it is mandatory.


🧠 Quick Recap

✔ useReducer manages complex state
✔ State updates happen via actions
✔ Reducers are pure functions
✔ Logic becomes predictable
✔ Excellent for forms & workflows


🎉 Conclusion

useReducer is not about writing more code —
it’s about writing better, clearer, and scalable code.

Once you understand it:

  • State logic stops being messy
  • Bugs reduce dramatically
  • Code becomes self-documenting

This lesson marks your entry into truly advanced state logic ⚛️🧠


👉 Next Lesson

Lesson 5 — useRef Hook (Beyond DOM Access)

Leave a Comment