The useReducer Hook

The useReducer Hook is an alternative to useState for managing state. While useState is perfect for simple values, useReducer is a better choice when the state logic is complex — involving multiple sub-values or multiple ways the state can be updated.

It is inspired by the concept of a reducer from the Redux state management pattern, but it is built directly into React and requires no external library.

When Should useReducer Be Used?

Consider using useReducer instead of useState when:

  • State has multiple related fields that are updated together
  • The next state depends on the previous state in complex ways
  • There are many different actions that can change the state
  • The state logic is becoming hard to read with multiple useState calls

Core Concepts

To understand useReducer, three key concepts are involved:

  • State — The current data the component holds
  • Action — A plain JavaScript object that describes what happened. It usually has a type property and optionally a payload (the data for the update)
  • Reducer — A pure function that takes the current state and an action, and returns the new state

A reducer function follows this structure:


function reducer(state, action) {
  switch (action.type) {
    case "ACTION_TYPE":
      return { ...state, updatedField: newValue };
    default:
      return state;
  }
}

Basic Syntax of useReducer


const [state, dispatch] = useReducer(reducer, initialState);
  • state — The current state value
  • dispatch — A function used to send actions to the reducer
  • reducer — The function that defines how state changes
  • initialState — The starting value of the state

Example — Counter with useReducer

Starting with a familiar counter example to compare it with useState:


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

function Counter() {
  const [state, dispatch] = useReducer(counterReducer, { count: 0 });

  return (
    <div>
      <p>Count: {state.count}</p>
      <button onClick={() => dispatch({ type: "increment" })}>+</button>
      <button onClick={() => dispatch({ type: "decrement" })}>-</button>
      <button onClick={() => dispatch({ type: "reset" })}>Reset</button>
    </div>
  );
}

When a button is clicked, dispatch sends an action object to the reducer. The reducer receives the current state and the action, then returns the new state based on the action's type. React updates the component with the new state.

Example — To-Do List with useReducer

This example demonstrates a more realistic use case where state involves multiple operations on a list:


import { useReducer, useState } from 'react';

const initialState = { todos: [] };

function todoReducer(state, action) {
  switch (action.type) {
    case "add":
      return {
        todos: [...state.todos, { id: Date.now(), text: action.payload, done: false }],
      };
    case "toggle":
      return {
        todos: state.todos.map((todo) =>
          todo.id === action.payload ? { ...todo, done: !todo.done } : todo
        ),
      };
    case "remove":
      return {
        todos: state.todos.filter((todo) => todo.id !== action.payload),
      };
    default:
      return state;
  }
}

function TodoApp() {
  const [state, dispatch] = useReducer(todoReducer, initialState);
  const [inputText, setInputText] = useState("");

  function handleAdd() {
    if (!inputText.trim()) return;
    dispatch({ type: "add", payload: inputText });
    setInputText("");
  }

  return (
    <div>
      <h3>My Tasks</h3>
      <input
        value={inputText}
        onChange={(e) => setInputText(e.target.value)}
        placeholder="Add a task..."
      />
      <button onClick={handleAdd}>Add</button>

      <ul>
        {state.todos.map((todo) => (
          <li key={todo.id}>
            <span
              style={{ textDecoration: todo.done ? "line-through" : "none" }}
              onClick={() => dispatch({ type: "toggle", payload: todo.id })}
            >
              {todo.text}
            </span>
            <button onClick={() => dispatch({ type: "remove", payload: todo.id })}>
              Delete
            </button>
          </li>
        ))}
      </ul>
    </div>
  );
}

The reducer cleanly handles three different operations on the same list: adding, toggling completion, and removing. All of this state logic lives in one predictable function, making it easy to understand and test.

The payload Property

When an action needs to carry data — such as the text of a new to-do or the ID of an item to delete — that data is placed in a payload property of the action object. The reducer then reads action.payload to use that data:


dispatch({ type: "add", payload: "Buy groceries" });
dispatch({ type: "remove", payload: 1234 });

useReducer vs useState

  • Use useState for simple, independent values like a number, a string, or a boolean.
  • Use useReducer when state is an object with multiple related fields, or when there are several distinct ways the state can change.
  • useReducer makes state transitions explicit and predictable — each action type clearly maps to a state change.

Key Points

  • useReducer is an alternative to useState for managing complex state logic.
  • A reducer is a pure function that takes the current state and an action, and returns the new state.
  • Actions are plain objects with a type property and an optional payload.
  • The dispatch function sends actions to the reducer to trigger state updates.
  • Centralizing state logic in a reducer function makes code easier to maintain and test.

The next topic covers Custom Hooks — how to extract reusable logic from components into standalone Hook functions.

Leave a Comment

Your email address will not be published. Required fields are marked *