The useReducer hook is usually recommended when the state becomes complex, with state values depending on each other or when the next state depends on the previous one. However, many times you can simply bundle your separate useState statements into one object and continue to use useState. So when is the extra boilerplate required for useReducer justified after all?

When there is complex business logic

Structuring your code with actions allows for better encapsulation of business logic.

Also, it allows you to separate the description of the action (WHAT happened) from how the state should update because of it (HOW it should be handled).

A simple example to showcase this is updating the state when the user navigates to the next page on a paginated list view.

With the hook, this could look like this:

setState(state => { ...state, page + 1})

However, with actions, you can add more semantics to this event and encapsulate the logic:

// action
dispatch({ type: 'GO_TO_NEXT_PAGE' })
// reducer
case 'GO_TO_NEXT_PAGE':
  return { ...state, page: state.page + 1}

In the future, if you decide something else needs to happen when the user clicks the "Next page" button, you only need to update one place: the reducer.

While this is a simple example, for deep nested values or arrays of objects, abstracting away the state update logic can be very valuable.

When there are many ways the state can be updated

In most situations, there are only a couple events that update the state - take for an example a simple login flow, where you only need to track loading, success and error states. In this case,  useReducer does not add that much value.

However, imagine a more complex login process, like multi-factor authentication, where the user needs to be guided over several screens - to enter his password, to enter the one time token - and you need to handle all sorts of edge cases like the QR code expiring, the password being incorrect etc.

In these situations reducers can really make it easier to follow the different component states and how they change.

When you need better debugging

Tracking how the component state changes after each setState  is generally harder to do than just having all changes go through a reducer.

Regardless if you prefer using console.log  or breakpoints, it's easier to add just one log/breakpoint in the reducer as opposed to one for each  call sprinkled across the component.

And if you ever decide to switch to Redux, the Redux DevTools make for a great debugging experience.

To wrap up, you don't need to jump to useReducer as soon as you have multiple useState hooks in your component. Many times, it's enough to just bundle all the state values in one object. However, if it becomes hard to figure out what's going on, useReducer can be a very helpful tool!

If you're interested to read more, I've found these articles helpful in my research: