useReducer

Manage complex state transitions with a reducer function.

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

useReducer is an alternative to useState for state with complex update logic or many related fields. You describe how state changes in a single reducer function.

How it works

  • A reducer takes the current state and an action, and returns the next state.
  • You trigger changes by calling dispatch({ type: '...' }).
  • All update logic lives in one place, which is easier to test and reason about.

Example

Example Β· javascript
import { useReducer } from 'react';

function reducer(state, action) {
  switch (action.type) {
    case 'inc':
      return { count: state.count + 1 };
    case 'reset':
      return { count: 0 };
    default:
      return state;
  }
}

function Counter() {
  const [state, dispatch] = useReducer(reducer, { count: 0 });
  return (
    <div>
      <p>{state.count}</p>
      <button onClick={() => dispatch({ type: 'inc' })}>+1</button>
      <button onClick={() => dispatch({ type: 'reset' })}>Reset</button>
    </div>
  );
}

When to use it

  • A shopping cart manages items through a useReducer so ADD_ITEM, REMOVE_ITEM, and CLEAR_CART actions keep all update logic in one auditable place.
  • A multi-step form uses a reducer to handle NEXT_STEP, PREV_STEP, SET_FIELD, and SUBMIT actions so the transition logic is centralised and unit-testable.
  • A real-time game state tracks player position, score, and lives in a single reducer so any game event can update multiple fields atomically.

More examples

Counter with useReducer

Defines a reducer with three action types and dispatches them from buttons β€” the canonical useReducer pattern.

Example Β· jsx
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: return state;
  }
}

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

Cart reducer with payload

Shows a reducer that handles ADD with a payload, REMOVE by filtering on an ID, and CLEAR to reset to empty.

Example Β· jsx
function cartReducer(state, action) {
  switch (action.type) {
    case 'ADD':
      return [...state, action.item];
    case 'REMOVE':
      return state.filter(i => i.id !== action.id);
    case 'CLEAR':
      return [];
    default:
      return state;
  }
}

import { useReducer } from 'react';
function Cart() {
  const [items, dispatch] = useReducer(cartReducer, []);
  return (
    <button onClick={() => dispatch({ type: 'ADD', item: { id: 1, name: 'Book' } })}>
      Add Book
    </button>
  );
}

Reducer with complex nested state

Manages a multi-step form with nested state using one reducer, keeping step navigation and field updates together.

Example Β· jsx
const initial = { step: 0, data: { name: '', email: '' } };

function wizardReducer(state, action) {
  switch (action.type) {
    case 'SET_FIELD':
      return { ...state, data: { ...state.data, [action.field]: action.value } };
    case 'NEXT':
      return { ...state, step: state.step + 1 };
    case 'BACK':
      return { ...state, step: Math.max(0, state.step - 1) };
    default:
      return state;
  }
}

import { useReducer } from 'react';
function Wizard() {
  const [state, dispatch] = useReducer(wizardReducer, initial);
  return <p>Step {state.step}: {state.data.name}</p>;
}

Discussion

  • Be the first to comment on this lesson.