Table of contents
useState hook as we discussed in one part of the series, helps in handling state management. However, useState is used for simple state management. When the state gets complex, it becomes uneasy to manage and can get quite confusing.
In useState, we know that the state, callback, and rendering are all in the body of the component. Now, imagine if we have to manage several form input states in our React application with different logics for each, the component gets larger, bulky and error-prone. This type of situation can break our application at the slightest update or refactoring of code. This is when the useReducer() hook comes to play.
In this tutorial, I will elaborate on the useReducer hook: what it does, how it works and other related questions that may pop up in your mind.
useReducer()
useReducer is the hook that helps you add a reducer to manage state in your component. Are you thinking "What is a reducer " ?. A reducer is a function that helps you update the state. You don't get it still? Don't worry, we are in this together. As we dive deeper, you'll get a full understanding of this hook and the different concepts around it.
The hook is just like the useState() hook, only that it helps manage complex state situations. When you have to manage several states, the useReducer() offers a more robust environment to manage the logic of your state.
While in useState, the whole body of work: logic, rendering etc is lumped into the component, useReducer offers a different playing field for each aspect of the component. With this, it's easier to read the code, manage the state and render the component. As we dive deeper into the course, you will understand it a bit further than it sounds now.
Now, let's move on to the technicalities of our discussion.
Important Concepts to Note
Honestly, you'll come across some concepts and words that may likely sound strange to you, but believe me, they aren't too difficult to grasp as they sound. You'll come across them in every step of your state management with useReducer.
Like useState() where you declare a state in an array. The first being the initialState and the second being the updater function.const [state, setState] = useState("").
With useReducer, it isn't much different. To declare a reducer, you do this:
import React, { useReducer } from 'react'
const [state, dispatch] = useReducer(reducer, initialState)
Now, let's discuss these concepts and arguments.
state: This is the current state. It is where the initialState is triggered at the first render.
dispatch: This is the function that helps you update your current state from its initialState to a new value. It fires the action type that was declared for the update. Once this is done, the component re-renders as the state has been updated.
reducer: This is the reducer function that specifies how the state is updated. It takes in two arguments: state and action. The state is the current state in question while the action is the type of activity that will be performed to update the state. I know this is a bit hard to grasp, but I'll explain with a counter app for a better understanding.
initialState: This is the initial state. It usually comes as an object holding the initial value of the state.
Now, let me use a simple counter app to explain these concepts, as I do realize it wouldn't be easy to grasp without a visible example. Let's get into it.
import React, { useReducer, useState } from 'react'
const initialState = 0
const reducer = (state, action) => {
switch (action) {
case "add":
return state + 1;
case "subtract":
return state - 1;
case "reset":
return initialState;
default:
throw new Error("Invalid action")
}
};
const Test = () => {
const [state, dispatch] = useReducer(reducer, initialState)
return (
<div>
<h2>{state}</h2>
<button onClick={() => dispatch("add")}>
add
</button>
<button onClick={() => dispatch("subtract")}>
subtract
</button>
<button onClick={() => dispatch("reset")}>
reset
</button>
</div>
)
}
export default Test
In our counter app, we can see all the concepts and arguments I explained earlier in play. They all fit in one place or the other carrying out their function in making sure the app works as it should.
Let's start from the top of the code.
We can see the initialState being initialized. In most cases, it usually comes as an object, but in our case, it is a number. It all depends on the type of state or operation you're trying to update. Instead of having to initialize the initialState to a value of 0 at the top of the code, this would work too;const [state, dispatch] = useReducer(reducer, 0).
Next, the reducer function. It took in two (2) arguments: state and action. The state being the initialState, and the action being the type of operation we intend on carrying out on our counter app. We can see several action types(add, subtract, reset) that we could carry out on the application.
Then in the JSX, we can see the dispatch rolling out our action types in the button to update the state of our counter.
Now, I am going to refactor the code so you can choose what way to write it. It all depends on your choice.
import React, { useReducer, useState } from 'react'
const initialState = {count:0}
const reducer = (state, action) => {
switch (action.type) {
case "add":
return {count: state.count + 1};
case "subtract":
return {count: state.count - 1};
case "reset":
return {count: initialState.count};
default:
throw new Error("Invalid action")
}
};
const Test = () => {
const [state, dispatch] = useReducer(reducer, initialState)
return (
<div>
<h2>{state.count}</h2>
<button onClick={() => dispatch({type: "add"})}>
add
</button>
<button onClick={() => dispatch({type: "subtract"})}>
subtract
</button>
<button onClick={() => dispatch({type: "reset"})}>
reset
</button>
</div>
)
}
export default Test
You can copy the code and try it out. It works just fine.
When to Use useReducer
As I discussed at the beginning of the tutorial, useReducer is best applicable when you have multiple or complex state situations. I will show an example of a form where we need to add a new user to our list of users. I'll show a useState case and then apply the useReducer case.
import React, { useState, useEffect } from 'react'
const Test = () => {
const [name, setName] = useState("")
const [age, setAge] = useState()
const [address, setAddress] = useState("")
const [occupation, setOccupation] = useState("")
const [maritalStatus, setMaritalStatus] = useState("")
const handleForm = (e) => {
e.preventDefault();
setName(name)
setAge(age)
setAddress(address)
setMaritalStatus(maritalStatus)
setOccupation(occupation)
}
return (
<div>
<form onClick={handleForm}>
<h3>Employee Information</h3>
<div>
<label htmlFor='name'>Name</label>
<input type='text' value={name}
onChange={(e)=> setName(e.target.value)}/>
</div>
<div>
<label htmlFor='age'>Age</label>
<input type='number' value={age}
onChange={(e)=> setAge(e.target.value)}/>
</div>
<div>
<label htmlFor='address'>Address</label>
<input type='text' value={address}
onChange={(e)=> setAddress(e.target.value)}/>
</div>
<div>
<label htmlFor='occupation'>Occupation</label>
<input type='text' value={occupation}
onChange={(e)=> setOccupation(e.target.value)}/>
</div>
<div>
<label htmlFor='maritalStatus'>maritalStatus</label>
<input type='text' value={maritalStatus}
onChange={(e)=> setMaritalStatus(e.target.value)}/>
</div>
<button>SUBMIT</button>
</form>
</div>
)
}
export default Test
This is a typical form using useState. In this form, we have 5 state values that need to be updated. You can see how clogged the component is.
Now we are going to use the useReducer approach below to update our states, and you can have a clearer understanding of the slightest reason why this may be better.
import React, { useState, useEffect, useReducer } from 'react'
const formState = {
name: "",
age: "",
address: "",
occupation: "",
maritalStatus: ""
}
const employeeReducer = (state, action) => {
switch (action.type) {
case "addNewName":
return {
...state, name: action.payload
}
case "addNewAge":
return {
...state, age: action.payload
}
case "addNewAddress":
return {
...state, address: action.payload
}
case "addNewOccupation":
return {
...state, occupation: action.payload
}
case "addNewMaritalStatus":
return {
...state, maritalStatus:action.payload
}
default:
return state;
}
}
const Test = () => {
const [state, dispatch] = useReducer(employeeReducer, formState)
const handleForm = (e) => {
e.preventDefault();
console.log(state);
};
return (
<div>
<form>
<h3>Employee Information</h3>
<div>
<label htmlFor='name'>Name</label>
<input type='text' name='name' value={state.name}
onChange={(e) => dispatch({type: "addNewName", payload:e.target.value})} />
</div>
<div>
<label htmlFor='age'>Age</label>
<input type='number' name='age' value={state.age}
onChange={(e) => dispatch({type: "addNewAge", payload:e.target.value})} />
</div>
<div>
<label htmlFor='address'>Address</label>
<input type='text' name='address' value={state.address}
onChange={(e) => dispatch({type: "addNewAddress", payload:e.target.value})} />
</div>
<div>
<label htmlFor='occupation'>Occupation</label>
<input type='text' name='occupation' value={state.occupation}
onChange={(e) => dispatch({type: "addNewOccupation", payload:e.target.value})} />
</div>
<div>
<label htmlFor='maritalStatus'>maritalStatus</label>
<input type='text' name='maritalStatus' value={state.maritalStatus}
onChange={(e) => dispatch({type: "addNewMaritalStatus", payload:e.target.value})} />
</div>
<button onClick={handleForm}>SUBMIT</button>
</form>
</div>
)
}
export default Test
I know you are thinking this method is much bulkier than using useState. Yes I agree, It's just an easier way to separate code into smaller chunks. There will arise times when you have to deal with much more state than these five and you'll find this method easier to update each state in real-time.
If you find this tutorial helpful, don't forget to follow me and like it. There are other parts of the React series on my page, you can go through them for your learning benefits.
Have any questions? Feel free to hola at me.
Please enjoy your weekend friends.