DevPath · Learn to code ESPTEN

useReducer and Context

useReducer: state with clear transitions

When useReducer instead of useState?

useState is perfect for simple, independent values. But when the state is complex (several related fields) or the updates follow clear transitions (several distinct ways to change it), spreading the logic across many setX calls becomes fragile and hard to follow.

useReducer concentrates all that logic in a single function: the reducer.

The reducer: (state, action) => nextState

A reducer is a pure function that receives the current state and an action, and returns the next state. It does not mutate the state: it builds and returns a new one.

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

dispatch: sending actions

In the component, useReducer(reducer, initialState) returns a pair: the current state and a function called dispatch. To change the state, you "dispatch" an action describing what happened:

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

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

The action is usually an object with a type property (and, if needed, extra data such as payload). The advantage: the logic lives in a single place, it is easy to read and to test, and the components only say what happened, not how the state changes.

In the exercises, useReducer is already available: use it directly, without importing it.

Put this into practice

DevPath is a hands-on course: you read the theory here; in the app you put it into practice with exercises that really run, offline.

Start free in the app →
Context API: avoiding prop drilling →