Divine Orji
Introduction to React useReducer
useReducer is a React Hook that gives us more control over state management than useState, making it easier to manage complex states. Its basic structure is:
Combined with other React Hooks such as useContext, it works almost similarly to Redux. The difference is that Redux creates a global state container (a store), while useReducer creates an independent state container within our component.
React useReducer can be used to manage state that depends on previous states, and efficiently manage multiple, complex states.
Prerequisites
To understand this article, the following are required:
- Good knowledge of JavaScript and React, with emphasis on functional programming.
- Experience with some React Hooks such as useState is not strictly required, but preferred.
How does useReducer work?
To understand this, let’s first look at JavaScript’s Array.prototype.reduce() method.
Given an array, the reduce() method executes a reducer callback function on each element in the array and returns a single final value.
The reduce() method takes in two parameters: a reducer function (required) and an initial value (optional). Take a look at this example below:
On its first iteration, prev takes in the initialValue, curr takes the current element in the array (the first element in this case), and the function executes with both values. The result is then stored as the new prev, and curr becomes the next element in the array.
If there is no initial value, prev starts with 0.
React useReducer works in a similar way:
- It accepts a reducer function and an initialState as parameters.
- Its reducer function accepts a state and an action as parameters.
- The useReducer returns an array containing the current state returned by the reducer function and a dispatch for passing values to the action parameter.
The reducer function
It is a pure function that accepts state and action as parameters and returns an updated state. Look at its structure below:
The action parameter helps us define how to change our state. It can be a single value or an object with a label (type) and some data to update the state (payload). It gets its data from useReducer's dispatch function.
We use conditionals (typically a switch statement) to determine what code to execute based on the type of action (action.type).
Understanding useReducer with examples
Example 1: A simple counter
In the code above:
- Our initialState is 0, so when the component is initially displayed on the browser, <h1>{state}</h1> shows 0.
- When a user clicks on the button, it triggers the dispatch, which sets the value of action to 1, and runs the reducer function.
- The reducer function runs the block of code inside it, which returns the sum of the current state and the action.
- Its result is then passed as the new, updated state and displayed on the browser.
Right now, our reducer function only increments the state based on the value in dispatch. What if we wanted our state also to decrement or reset? Let’s modify the reducer function to fit our use case.
Example 2: Counter with extra steps
In the code above:
- The value in each dispatch is an object with type and payload.
- In our reducer function, we used a switch statement to determine our changes to the state based on a chosen type of action.
For a deeper understanding of how we can use useReducer, let’s take a look at some more complex examples.
Example 3: Smart Home controls
Write state logic for a smart home app that controls appliances like light bulbs, AC, music, television etc.
Here’s how we will structure our code:
Here’s our initial state:
Let’s create a reducer to deactivate or activate a chosen appliance:
Now let’s import and set up useReducer to manage our state:
Here, useReducer takes in our initial state (appliances) and our reducer function, and returns the current state, and a dispatch function that we can use to update our reducer’s action.
Let’s render the data in our state on the UI:
Here we get the state returned from useReducer, map it and display each appliance name and status (active or inactive). Here’s the result:
Let’s add some functionality to our UI code using dispatch to update the state:
Here, when a user clicks on an Active button, it deactivates the appliance and displays the Not active button.
Example 4: Shopping cart
Let’s write some logic to manage the state of a shopping cart with various items in it. We need to give our users the ability to add items to the cart or delete the ones they don’t want.
First let’s set up our initial state:
On a closer look at our initial state, we will see that it has two states inside: input that contains an empty string and items that contains an empty array. Our input will hold whatever the user types in, and items will contain an array of chosen items. This means that we’re handling multiple states.
Next, let’s import and set up useReducer and build out the UI to get cart input from the user:
Here, we created an input that takes its value from our useReducer's state. Right now, the value is an empty string due to our initialState, but when we update the state, it’ll take in the updated value.
Our handleChange and handleSubmit are going to contain dispatch functions to get and submit the value from our input:
In our handleChange function, we have a dispatch function that contains two values:
- type: 'input', which specifies the type of action.
- payload, which contains the value gotten from our <input>. We will update our state object with this value.
In handleSubmit, our payload will create a new object with two properties:
- id: Its value will be gotten from the current date.
- name: This will contain the value of the input in our current state.
Based on its type, our reducer function will add the content of this payload to the items array in our state.
Let’s write the reducer function to handle all these data:
Let’s also update our UI code to display the items in our state:
Let’s now give users the ability to delete the items they don’t want anymore.
Update our reducer function:
Adding a delete button to the UI:
And we’re done! Here’s the complete code below:
https://gist.github.com/dpkreativ/25f42715db4da07262c988bf633cd192
https://gist.github.com/dpkreativ/25f42715db4da07262c988bf633cd192
useState vs useReducer
Fundamentally, we can manage our state with either useState or useReducer. As a matter of fact, useState implements useReducer under the hood.
The significant difference between both hooks is that with useReducer, we can avoid passing callbacks through different levels of our component and use the dispatch function instead, improving performance for components with deep updates.
Declaring the state
We declare useState like this:
In useReducer, it is done like this:
Updating state
In useState, we update our state like this:
In useReducer, we do it like this:>
The value of newState is then sent to our reducer function, which updates the state.
When to use useReducer Hook
The current state depends on the previous state
In Example 2, we increment or decrement the counter based on the previous value in the state. useReducer gives a predictable state transition when moving from one state to another.
Managing complex states or multiple states
When working with complex states such as nested objects, useReducer helps us determine how the state is updated based on the particular action triggered. In Example 4, its state is an object which contains a parameter (items) that is an array of objects. With useReducer, we assigned action types such as 'add', 'input', and 'delete' to update different parts of the state.
Updating state based on another state
In Example 4, to add a new item to the items array, we need to get the input value from the user first. With useReducer, we get the current input value from our state and use it to create a new object, which we then added to our items array.
When not to use the useReducer Hook
Managing simple state
It would be much quicker to set up useState for cases where we need to handle a very simple state.
Central state management
In a large application with many components depending on the same state, it would be better to use a third party state management like Redux or Mobx, as they centralize our app’s state and logic.
Conclusion
This article demonstrated how to handle complex states with useReducer, exploring its use cases and tradeoffs. It’s important to note that no single React hook can solve all our challenges, and knowing what each hook does can help us decide when to use it.
The examples used in this article are available on CodeSandbox:
Add memberships to your Webflow project in minutes.
Over 200 free cloneable Webflow components. No sign up needed.
Add memberships to your React project in minutes.