useReducer

The useReducer hook is best used in scenarios where you are manipulating state in a way that is too complex for the trivial React_useState use case. useState is best employed when you are updating a single value or toggling a boolean. If you are updating the state of an object or more complex data structure, it is often more efficient to employ useReducer.

This makes the code more manageable and also helps with separating state management from rendering.

Syntax

const [state, dispatch] = useReducer(reducer, initialState);
  • initialState
    • The starting state, typically an object
  • reducer
    • A pure function that accepts two parameters:
      • The current state
      • An action object
    • The reducer function must update the current state (immutably) and return the new state
    • We can think of the reducer as working in the same manner as state/setState in the useState hook. The functional role is the same, it is just that the reducer offers more than one type of update.

Example reducer

function reducer(state, action) {
  let newState;
  switch (action.type) {
    case "increase":
      newState = { counter: state.counter + 1 };
      break;
    case "descrease":
      newState = { counter: state.counter - 1 };
      break;
    default:
      throw new Error();
  }
  return newState;
}

In this example we are updating an object with the following shape:

{
    counter: 0,
}

This would be the initialState that we pass to the useReducer hook along with a reference to reducer above.

To update the state we would invoke the dispatch function which applies one of the actions defined in the reducer. For example the following dispatch increments the counter by one:

dispatch({ type: "increase" });

To view the updated value:

console.log(state.counter);

Refining the syntax

Because React doesn’t mutate state, the reducer doesn’t directly modify the current state in the state variable, it creates a new instance of the state object on each update.

In the reducer example above this is achieved by declaring a variable newState that is updated by each action type and then returned. There is a more elegant way of doing this using spread syntax:

function reducer(state, action) {
  switch (action.type) {
    case "increase":
      return { ...state, counter: state.counter + 1 };
      break;
    case "decrease":
      return { ...state, counter: state.counter - 1 };
      break;
    default:
      throw new Error();
  }
}

Including payloads

In the examples so far, we have updated the the state directly via the action type however it is also possible to pass data along with the action.type as action.payload.

For example:

dispatch(
  {
    type: 'increase_by_payload'
    payload: 3,
  });

Then we would update our reducer to handle this case:

function reducer(state, action) {
  switch (action.type) {
    ...
    case 'increase_by_payload':
    return {...state, counter: state.counter + action.payload}
    default:
      throw new Error();
  }
}