Cleaning Complex State With React’s useReducer

You might reach for Redux or use React’s Context when you need to manage state across multiple components, but when it comes to a single component’s state, the most likely option that you’ll reach for is React’s useState hook. But what happens when your component needs to manage complex state?

Using useState

Let’s take a look at an example of how a seemingly simple component can harbor quite a bit of complexity:

On the surface, this form looks quite simple. However, the complexity stems from managing all the possible variations of state that need to be accounted for:

  • email

  • password

  • is submitting

  • errors

  • field validations

We can manage all of this state with useState, but that requires keeping track of all of this related state imperatively. Let’s take a look.

Using useState, we can define the state for our component:

Once we’ve defined our initial state, let’s take a look at how we could manage this state in the simple form example:

Let’s focus on the isSubmitting property as an example of how useState contributes to this pattern of complexity.

When our form is initially rendered, isSubmitting is set to false. Our component uses the isSubmitting property in two ways:

  • to adjust the disabled state of the form’s submit button

  • to set the text inside of the button depending on whether a user has submitted the form or not.

The isSubmitting property that the button relies on is set and unset in the onSubmit method. When a user submits the form, it will send a request to create a new user. It sets the value as loading when the form is submitted and unsets the loading state when:

  • either the email or password field is blank

  • the request to create a user was successful

  • the request to create a user returns an error

As you can see, it becomes quite cumbersome to manage and keep track of this one property. It becomes even more so when dealing with multiple properties. It’s also extremely difficult to reason about how all of this state is related. Luckily, React’s useReducer provides a way to simplify these cases of complex state management.

useReducer

React’s useReducer is similar to useState in that it provides a way to manage a component’s state. However, it’s different in the way it handles this as useReducer follows a similar pattern to Redux. With useReducer, you have:

  • a state object with multiple properties

  • a reducer function

With useReducer, instead of being concerned about the details of our component’s state changes, we can rely instead on the actions that the user can take:

  • create a user

  • creating a user

  • successful submission

  • error on submission

To summarize: useReducer takes some state and instructions (actions) on how to update that state. It then returns the updated state.

Let’s simplify the above example by refactoring it with useReducer.

State Object

Let’s create our initial state object:

Reducer Function

Next, we can create our reducer function. The reducer function accepts a state object and an action that describes how to update the state. In our reducer we rely on a switch statement based on the action’s type property. These types correspond to the types of actions that a user can take with our application:

Within each case, we can directly define how we expect the state to change based on the action that was dispatched, all in one place:

The reducer function returns an updated copy of that state. This practice of updating a copy of the state, rather than the state directly, is known as immutability. We never return state directly, but rather an updated copy of state.

Once implemented, we can now see how useReducer improves the readability and structure of our code:

Initial State:

Reducer:

Component:

Conclusion

In conclusion, useReducer separates what happens (the granularity of state changes scattered throughout the component) from how it happens (the user behavior that took place). This allows the “how it happens” to be centralized all in one easy to reason about place — within your reducer function.

This also provides consistency as you can dispatch an action from anywhere in the component and expect the same state every time.

I’ve found myself using useReducer more and more as it’s a much cleaner, more organized pattern when dealing with complex state management within components.

Previous
Previous

Understanding Elasticsearch

Next
Next

Measuring React Performance With the Profiler