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.
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.
Okay, now we know what useReducer
does, but why do we want to use useReducer
over useState
? There are several advantages:
useReducer
lets you organise complex state logic into a single reducer function, making it easier to reason about and maintain.useReducer
can help separate concerns and reduce coupling between components.useReducer
allows you to pass down state
and dispatch
functions through context, avoiding prop drilling.Despite the advantages of useReducer
, there are also some disadvantages too.
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.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.useReducer
, you must define an action type for every possible state change, which can be tedious and error-prone.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.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
.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.
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:
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.
Sign up to get updates when I write something new. No spam ever.
Subscribe to my Newsletter