React useReducer Hook

useReducer is an alternative to useState for managing state that involves multiple values or complex update logic. Instead of calling a setter function with a new value directly, you dispatch an action that describes what happened, and a reducer function decides how the state should change.

This pattern comes from Redux, but useReducer is built into React. You do not need any extra libraries to use it.

When to use useReducer

useState handles most cases well. Reach for useReducer when:

  • You have multiple pieces of state that change together
  • The next state depends on the previous state in a complex way
  • You have several different ways to update the same state and the logic is getting hard to follow
  • You want to make state updates more explicit and easier to trace

How it works

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

There are three parts:

  1. The state: the current value, like in useState
  2. The dispatch function: call this to trigger an update, passing an action object
  3. The reducer function: a pure function that takes (state, action) and returns the new state

The reducer lives outside the component. It should be a pure function with no side effects.

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

Example: counter with multiple actions

Here is a counter with increment, decrement, and reset, written with useReducer.

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 };
    case 'RESET':
      return initialState;
    default:
      throw new Error(`Unknown action: ${action.type}`);
  }
}

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

  return (
    <div>
      <p>Count: {state.count}</p>
      <button onClick={() => dispatch({ type: 'DECREMENT' })}>-</button>
      <button onClick={() => dispatch({ type: 'RESET' })}>Reset</button>
      <button onClick={() => dispatch({ type: 'INCREMENT' })}>+</button>
    </div>
  );
}

Notice that the component itself has no update logic. It just says what happened (INCREMENT) and the reducer decides what to do with it.

Example: todo list

This shows a more realistic case where multiple fields and multiple operations make useReducer a natural fit.

import { useReducer, useState } from 'react';

const initialState = { todos: [] };

function reducer(state, action) {
  switch (action.type) {
    case 'ADD_ITEM':
      return {
        todos: [
          ...state.todos,
          { id: Date.now(), text: action.text, completed: false },
        ],
      };
    case 'TOGGLE_ITEM':
      return {
        todos: state.todos.map(todo =>
          todo.id === action.id
            ? { ...todo, completed: !todo.completed }
            : todo
        ),
      };
    case 'DELETE_ITEM':
      return {
        todos: state.todos.filter(todo => todo.id !== action.id),
      };
    default:
      throw new Error(`Unknown action: ${action.type}`);
  }
}

export default function TodoList() {
  const [state, dispatch] = useReducer(reducer, initialState);
  const [inputText, setInputText] = useState('');

  function handleAdd(e) {
    e.preventDefault();
    if (!inputText.trim()) return;
    dispatch({ type: 'ADD_ITEM', text: inputText.trim() });
    setInputText('');
  }

  return (
    <div>
      <form onSubmit={handleAdd}>
        <input
          value={inputText}
          onChange={e => setInputText(e.target.value)}
          placeholder="Add a task..."
        />
        <button type="submit">Add</button>
      </form>
      <ul>
        {state.todos.map(todo => (
          <li key={todo.id}>
            <span
              onClick={() => dispatch({ type: 'TOGGLE_ITEM', id: todo.id })}
              style={{ textDecoration: todo.completed ? 'line-through' : 'none', cursor: 'pointer' }}
            >
              {todo.text}
            </span>
            <button onClick={() => dispatch({ type: 'DELETE_ITEM', id: todo.id })}>
              Delete
            </button>
          </li>
        ))}
      </ul>
    </div>
  );
}

Each action is a plain object with a type field. You can add any extra data the reducer needs (like text or id).

Passing data with actions

Actions are plain objects. Beyond type, you can include any additional data:

dispatch({ type: 'ADD_ITEM', text: 'Buy milk' });
dispatch({ type: 'DELETE_ITEM', id: 42 });
dispatch({ type: 'UPDATE_ITEM', id: 42, text: 'Buy oat milk' });

The reducer receives the full action object, so it has access to whatever you pass.

useState vs useReducer

SituationBetter choice
A single value (counter, boolean, string)useState
Several independent valuesMultiple useState calls
Several values that change togetheruseReducer
Update logic has many branches or conditionsuseReducer
You want to test state logic in isolationuseReducer (reducer is a plain function)

useReducer is not better than useState. It is more explicit, which is an advantage when the update logic is complex, and a disadvantage when it is not.

Common mistakes

Mutating state in the reducer

The reducer must return a new object. If you modify the existing state, React may not detect the change.

// Wrong: mutating existing state
case 'ADD_ITEM':
  state.todos.push(action.item); // Don't do this
  return state;

// Correct: return a new object
case 'ADD_ITEM':
  return { todos: [...state.todos, action.item] };

Using useReducer for simple state

If you only have one value and one way to update it, useState is simpler. Adding useReducer for the sake of it adds ceremony without benefit.

FAQ

Should I use useReducer or Redux?

For most apps, useReducer combined with React context is enough. Redux adds middleware, DevTools, and a global store, which are useful for very large apps with complex state shared across many components. Start with useReducer. Switch to Redux (or Zustand, Jotai, or another library) if you genuinely need features it cannot provide.

What is an action?

An action is a plain JavaScript object that describes something that happened. It must have a type field (a string). You can add any other fields you need. The convention comes from the Flux architecture and has been widely adopted because it makes state changes easy to log and understand.

How does this differ from useState?

With useState, you call the setter with the new value directly: setCount(5). With useReducer, you describe what happened and let the reducer decide the new value: dispatch({ type: 'INCREMENT' }). The reducer is what actually contains the logic.

What to learn next