In this article, we will discuss the useReducer hook of the ReactJS.
We will cover the following topics:
What is a useReducer hook?
How useReducer works?
When to use useReducer hook?
The useReducer hook is used to manage the state in your ReactJS application.
Yes, you can also use the useState hook for the same purpose. We will discuss the situations where you should prefer using useReducer hook over useState later in this article.
useReducer works on the concept of reducer function. In vanilla JS, when using arrays, you might have come across functions like map, filter, and reduce, which works on array instances.
Let’s understand the reducer function.
As defined on the mdn web docs:
The reduce() method executes a user-supplied “reducer” callback function on each element of the array, passing in the return value from the calculation on the preceding element. The final result of running the reducer across all elements of the array is a single value.
const array1 = [1, 2, 3, 4]
const reducer = (accumulator, currentValue) => accumulator + currentValue
// 1 + 2 + 3 + 4
console.log(array1.reduce(reducer))
// expected output: 10
// 5 + 1 + 2 + 3 + 4
console.log(array1.reduce(reducer, 5))
// expected output: 15
In the above example, we have an array of 4 numbers. Next, we have a function called reducer
, which accepts two parameters accumulator
and currentValue
returns a single value, a sum of the elements of the array.
If we call the reduce function by passing our reducer function it will sum up the array element and the result will be 10. In the next log statement, we can see that it accepts an extra parameter as 5. Which will set the accumulator’s default value as 5 and the result will be 15.
So, remember two things from here.
- The reducer function accepts two parameters first if reducer function.
- The initial value that the reducer function can make use of.
To summarise the job of the reducer function is to accept two paraments and reducing them down to a single value.
How useReducer works?
So, we got a refresher on the concept of reducer in JS. useReducer hook also follows a similar concept but it returns an array containing two elements.
A state
object containing the latest state and dispatch
function to dispatch an action.
const [state, dispatch] = useReducer(reducer, initialArg, init)
To get it working, we need to do a few things:
- Define an initial state.
- Provide a function that contains actions that update the state.
- Trigger useReducer to dispatch an updated state that’s calculated relative to the initial state.
Using useReducer hook
Let’s understand the usage of useReducer hook with the help of an example. Here we will use useReducer to manage the state of input elements of our demo application.
const reducer = (state, action) => {
const { type, payload } = action
switch (type) {
case "name-change":
return { ...state, name: action.payload.name }
case "ageGroup-change":
if (payload.ageGroup === "above50") {
return {
...state,
ageGroup: payload.ageGroup,
discount: 10,
amountPayable: getAmountPayable(0.1),
}
}
return {
...state,
ageGroup: payload.ageGroup,
discount: 5,
amountPayable: getAmountPayable(0.05),
}
default:
console.log("some other action")
}
}
Here we have created a reducer function that accepts state and action.
Our action has two properties type and state. type property of action tells us type of action being fired. Based on action type we decide how to update the current state and return the updated state.
const initialState = {
name: "",
ageGroup: "",
discount: 0,
itemPrice: 1000,
amountPayable: 0,
}
Then we define our initializer object, which is the initial state of our application.
const [state, dispatch] = useReducer(reducer, initialState)
Here we are calling useReducer hook with the first argument as reducer function and the second argument as initializer object.
This function returns an array containing two things: the state, and an action dispatcher to update the state. Then we are grabbing the state and dispatch it in an array using the array destructuring.
Dispatching action
<input
type="radio"
id="below50"
name="below50"
value="below50"
checked={state.ageGroup === "below50"}
onChange={e => {
dispatch({
type: "ageGroup-change",
payload: { ...state, ageGroup: e.target.value },
})
}}
/>
Here we are dispatching the action on the trigger of the onChange function. The dispatch function accepts an object containing the type of the action and the payload as its properties.
It’s not mandatory to name the properties as type and payload you can name it anything you want. Also, the payload is optional.
Similarly, we have actions being dispatched for other onChange events on different input elements. (Those actions are being checked for in our reducer via the switch statement and case clauses)
<input
type="radio"
id="below50"
name="below50"
value="below50"
checked={state.ageGroup === "below50"}
onChange={(e) => {
dispatch({
type: "ageGroup-change",
payload: { ...state, ageGroup: e.target.value }
});
}}
/>
/>
We are using the state variable (our initial state and the updated state) to show the updated values on our input elements.
A working example can be found here codesandbox.io/s/gifted-carson-vj0rc?file=/..
Same example using setState hook codesandbox.io/s/gifted-carson-vj0rc?file=/..
Conclusion
The useReducer hook is extremely useful when different states depend on each other for example You fetching some data from an API, and you want to display:
- "A loader." while fetching it
- render data once you have it
- or an error message in case of an error
You'll want all three of these to be in sync with each other. If you get the data, you want to make sure it's not loading and there's no error. If you get an error, it's not loading and there's no data. or you have a complex state to manage.
So instead of managing these three states using the setState hook, you can use useReducer instead and dispatch an action on a different status of the promise during the ajax call.
This way you can write all your state management logic at one please in your reducer function.