DevAcademy
LearnReactuseReducer
IntermediateReact

useReducer

Learn how to manage complex state transitions predictably using useReducer, an alternative to useState.

Reading Time

18 min

Lesson

Lesson 31 of 42

When useState Starts to Strain

A component with several related pieces of state, updated by many different event handlers, can become hard to follow — related updates get scattered across the component, and keeping them in sync involves multiple setState calls.

The Reducer Pattern

useReducer centralizes state updates into a single function called a reducer, which takes the current state and an "action" describing what happened, and returns the new state.

A Basic Counter with useReducer

import { useReducer } from "react";

function reducer(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:
      throw new Error("Unknown action: " + action.type);
  }
}

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

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

useReducer Return Value

ItemPurpose
stateThe current state value
dispatchA function to send an action describing what happened

Actions with a Payload

An action can carry extra data beyond just its type, letting the reducer make decisions based on the specifics of what happened.

An Action with a Payload

function reducer(state, action) {
  switch (action.type) {
    case "add_todo":
      return [...state, { id: Date.now(), text: action.payload, done: false }];
    case "toggle_todo":
      return state.map((todo) =>
        todo.id === action.payload ? { ...todo, done: !todo.done } : todo
      );
    default:
      return state;
  }
}

function TodoApp() {
  const [todos, dispatch] = useReducer(reducer, []);

  function addTodo(text) {
    dispatch({ type: "add_todo", payload: text });
  }

  // ...
}

useState vs useReducer

useStateuseReducer
Best forSimple, independent valuesComplex state with several related sub-values or transitions
Update logic locationScattered across event handlersCentralized in one reducer function
TestabilityHarder to isolateEasy — a reducer is a pure function you can test directly

Reducers Must Be Pure

A reducer should never mutate the existing state or cause side effects — given the same state and action, it must always return the same new state, computed as a new object/array rather than an in-place mutation.

Best Practice

Reach for useReducer once a component’s state updates start depending on each other, or when the next state depends on several parts of the previous state at once — it keeps that logic testable and in one place instead of scattered across handlers.

Interview Questions

Quick Quiz

1. What are the two values returned by useReducer?

2. What does a reducer function receive as arguments?

3. When is useReducer generally preferred over useState?