Skip to content

Edvins Antonovs

How to efficiently refactor useState to useReducer in React

As your React app grows in complexity, managing state with useState hooks can become challenging to maintain. That's where useReducer comes in – a React hook that can help organise complex state logic, enable better separation of concerns and make testing easier. In this blog post, we'll walk through the process of refactoring from useState to useReducer in React, using some concrete code examples (Counter and Shopping Cart) to illustrate the process.


What is useReducer?

useReducer is a React hook that lets you manage the state using a reducer function. A reducer function is a pure function that takes the current state and an action and returns the new state. It is a way of encapsulating state updates into a single function. Here's an example:

In this example, we've defined an initial state with a count of 0 and a reducer function that handles two actions: increment and decrement. We then use useReducer to initialise our state and dispatch function and use them to update the state when the user clicks on the + or - buttons.


Advantages of useReducer

Okay, now we know what useReducer does, but why do we want to use useReducer over useState? There are several advantages:

  • Better organisation — useReducer lets you organise complex state logic into a single reducer function, making it easier to reason about and maintain.
  • Separation of concerns — by encapsulating state updates into a single function, useReducer can help separate concerns and reduce coupling between components.
  • Better testing — since reducer functions are pure, testing them in isolation is easy.
  • Performance optimisation — by reducing the number of re-renders. This is because useReducer allows you to pass down state and dispatch functions through context, avoiding prop drilling.

Disadvantages of useReducer

Despite the advantages of useReducer, there are also some disadvantages too.

  • More verbose — Using useReducer often requires more code than using useState, especially when dealing with simple state updates. This can make your code harder to read and maintain.
  • Steep learning curve — The useReducer hook can be more challenging to understand and use effectively than useState. It requires a fundamental understanding of the reducer pattern, which may only be familiar to some developers.
  • More boilerplate — when using useReducer, you must define an action type for every possible state change, which can be tedious and error-prone.
  • Less flexible — useReducer is a more rigid solution than useState. It's best suited for managing complex state changes involving multiple data pieces but may need more than simple state updates.
  • Requires more planning — as useReducer requires a separate action type for each state update, you need to plan out your state management carefully before implementing it. This can make it more time-consuming to set up than useState.

Refactoring process

Now that we understand the basics of useReducer let's walk through the basic process of refactoring from useState to useReducer. Here's a simple example to get us started:

In this example, we have a simple Counter component that uses useState to manage the count state. When the user clicks the + or - buttons, we update the state with setCount.

To refactor this to useReducer, we need to define a reducer function and an initial state. Here's what the refactored code looks like:

In this refactored code, we've defined an initial state object { count: 0 }, and a reducer function that handles two actions INCREMENT and DECREMENT. We then use useReducer to initialise our state and dispatch function and use them to update the state when the user clicks on the + or - buttons.

Note that we've replaced the setCount call with a dispatch call that passes an object with a type property of 'INCREMENT'. This object represents the action we want to perform on our state. The reducer function then handles this action and returns the new state object.


Advanced example

Whilst Counter is a simple example which provides enough to understand in general how useReducer works. Let's look at ShoppingCart as a more complex example to see how useReducer can help us organise complex state logic.

Here's a simple shopping cart component that uses useState to manage the state:

In this example, we have a ShoppingCart component that uses useState to manage the cart state. The cart state is an object with two properties: items and total. The items property is an array of items in the cart, and the total property is the total price of all the items in the cart. When the user clicks the + or - buttons, we update the state with setCart.

In order to prevent issue with duplicate id values, we use a useRef hook to keep track of the current id value and increment it each time we add a new item to the cart.

To refactor this to useReducer, same here as in the previous example we need to define a reducer function and an initial state. Here's what the refactored code looks like:

Conclusion

Refactoring from useState to useReducer can be a great way to organise complex state logic in your React app. By encapsulating state updates into a single reducer function, you can make your code more maintainable, easier to test, and more scalable. In this blog post, we've walked through the process of refactoring a simple Counter component from useState to useReducer, and provided some concrete code examples to illustrate the process.

I hope you found this helpful because I recently refactored a big chunk of the code for one of the side projects I was working on. I reduced the number of re-renders from 10 to 2, a considerable performance boost for the app. The code was much easier to understand and maintain, and it was much easier to test the reducer functions in isolation. If you have any questions or comments, please leave them below.

© 2024 by Edvins Antonovs. All rights reserved.