State is an important part of a React application. Most functionalities involve making state updates in your component.

But as your application grows, state updates become more and more complex. These complex state updates might get overwhelming when you revisit your code.

There is a different way of handling state updates, and that's by using reducers. But what are reducers? How do you use them? What does the useReducer hook do? In this post, I'll answer all these questions.

What Is a Reducer and Why do You Need It?

Let's take an example of a To-Do app. This app involves adding, deleting, and updating items in the todo list. The update operation itself may involve updating the item or marking it as complete.

When you implement a todo list, you'll have a state variable todoList and make state updates to perform each operation. However, these state updates may appear at different places, sometimes not even inside the component.

To make your code more readable, you can move all your state updates into a single function that can exist outside your component. While performing the required operations, your component just has to call a single method and select the operation it wants to perform.

The function which contains all your state updates is called the reducer. This is because you are reducing the state logic into a separate function. The method you call to perform the operations is the dispatch method.

How the useReducer Hook Works

You can add a reducer to your component using the useReducer hook. Import the useReducer method from the library like this:

import { useReducer } from 'react'

The useReducer method gives you a state variable and a dispatch method to make state changes. You can define state in the following way:

const [state, dispatch] = useReducer(reducerMethod, initialValue)

The reducer method contains your state logic. You can choose which state logic to call using the dispatch method. The state can also have some initial value similar to the useState hook.

useReducer Hook Example

Let's take a simple example where we have a list of users. We can add a new user, delete an existing user, and update user details. Normally, we would create a state variable user and perform state updates at different places.

Let's try doing the same using reducers:

const [users, dispatch] = useReducer(reducerMethod, userData);

Use the following initial data:

const userData = [
    {
        id:1,
        name: 'kunal',
        age: 22,
        admin: true
    },
    {
        id:2,
        name: 'rounak',
        age: 23,
        admin: false
    },
    {
        id:3,
        name: 'utkarsh',
        age: 22,
        admin: false
    },   
]

How to Define the Reducer Method

The reducer method contains our state updates. The method takes two arguments, the current state value and an action object. The action object contains the type of the action and additional data needed to perform the update.

We'll perform three types of updates – for user added, updated, and deleted. We'll use switch-case to select the type of operation to be performed.

const reducerMethod = (users, action) => {
    switch(action.type) {
        // State updates here
    }
}

The type field contains the name of the operation to be performed. This is a string and you can set any value you want. Just make sure it's relevant to the action being performed for better readability. Let's perform the add operation first:

case 'addUser': {
    return [
        ...users,
        action.newUser
    ]
}

The logic for updating state is similar to setState. Here, you return a new state value rather than making changes to the state variable directly.

Let's perform the update operation now. While performing the update operation, the dispatch method passes an updatedUser object to update an existing user. This additional data is passed through the action object.

case 'updateUser': {
    return users.map(user => {
        if(user.id == action.updatedUser.id)
        	return action.updatedUser
        return user;
    })
}

Now, for the delete operation, the dispatch method passes only the id of the object so that the state array can filter it out.

case 'deleteUser': {
	return users.filter(user => user.id !== action.id)
}

Let's also have a default case if an action other than the above three is specified.

default: {
	// Handle error here
}

Now, let's create the components that would actually use this reducer.

Display the list of users in the UserDetails component with the following props:

<UsersList users={users}
           handleUpdateUser={handleUpdateUser}
           handleDeleteUser={handleDeleteUser}
 />

Also, create a form to add new users in the AddUserForm component.

<AddUserForm handleAddUser={handleAddUser} />

I have not mentioned the actual implementations of the components here, as the focus is only on the state update part.

We'll make the state updates inside the handler methods by calling the dispatch method and passing the type of the state update with some data. For the add operation, pass the new user to be added.

const handleAddUser = (user) => {
    dispatch({
        type: 'addUser',
        newUser: user
    })
}

Similarly, you can implement handleUpdateUser and handleDeleteUser.

const handleUpdateUser = (updatedUser) => {
    dispatch({
        type: 'updateUser',
        updatedUser: updatedUser
    })
}

const handleDeleteUser = (userId) => {
    dispatch({
        type: 'deleteUser',
        id: userId
    })
}

The newUser, updatedUser and userId are parameters passed from the AddUserForm and UsersList components. They contain the required data to make the state updates.

Conclusion

For any feature you create, state updates form a crucial part of the implementation in React. As the complexity of the application increases, so does the number of state updates.

In this post, I explained what a reducer is and why we need it. With the help of an example, I showed you how convenient it is to have all the state updates in one place in a separate function. This makes the code more readable and accessible.

I hope this tutorial helped eliminate any confusion regarding the useReducer hook. I have tried to explain it in very simple terms.

If you are unable to understand the content or find the explanation unsatisfactory, let me know. New ideas are always appreciated! Feel free to connect with me on Twitter. Till then, goodbye!