useReducer – Mastering React Hooks

useReducer – Mastering React Hooks

State management is the most important aspect of building React web applications, but it can sometimes be tricky. There are numerous methods for managing state logic at the component or application level, but the most effective one uses reducers and the useReducer Hook.

Reducers, along with the useReducer hook, provide a clean and efficient way to handle state logic.

Let’s get started and learn everything you need to know about reducers and the useReducer hook. We’ll go over a lot of examples and applications, so make sure to stick around until the end 🚀

Beginning with an Analogy

Let’s say you’re hungry and want a burger. You went to a shop to buy buns, then to another shop to get some veggies, then to another shop to buy some cheese and sauces.

Buying all the required ingredients
Buying all the required ingredients

Finally, you reach home, cut all the veggies using a chopper, toast the bun using a pan, put everything together, bake it in the microwave, and have the burger ready.

Making the burger
Making the burger

You were eating the burger, and it tasted delicious, but then you looked around your kitchen and saw the mess you have made preparing that burger 😬

Kitchen mess
Kitchen mess

To avoid this mess, you could have chosen an alternative method to satisfy your burger cravings, such as going to a restaurant and ordering a burger.

Giving an order in a restaurant
Giving an order in a restaurant

The waiter at the restaurant will collect your order and forward it to the chef. Now that the chef has access to all of the food items, it is his responsibility to make your burger and clean up the kitchen mess. You don’t have to worry about anything; simply place your order and relax.

Burger ready without much efforts
Burger ready without much efforts

Comparing the analogy with Reducers and useReducer hook

You can understand React’s reducer in a similar way. You are a programmer, and all the ingredients you need are various state variables. Ingredients represent different state variables. Chopper, Pan, and Microwave are functions used to modify state variables. The kitchen was your component.

Comparing our analogy with a React
Comparing our analogy with a React

Of course, having everything in the same component can make the component messy, just like it would be messy if you kept all your cooking tools in the same place.

As the number of states increase, component starts to become messy
As the number of states increase, component starts to become messy

React Reducers and useReducer hook work just like restaurants. The useReducer functions are like a restaurant that provides some waiters to take your orders. These waiters are called dispatch functions, and the orders are called actions.

Comparing React Reducers and the useReducer hook to a restaurant
Comparing React Reducers and the useReducer hook to a restaurant

Basically, you dispatch an action that changes the state. We’ll look into this in detail in further sections.

A reducer function is like a chef who actually prepares the order. They have access to all groceries, i.e., the state, and the order, i.e., the actions to take.

In other words, you just order, and the food is prepared. Since the restaurant is away from home, there is no kitchen clutter.

Wasn’t it easy? It is even easier to implement. Let’s learn more about reducers and how to use them.

Introduction to Reducers

Reducers are a way to reduce the component’s state logic to a single function that is stored outside the component.

These are simple JavaScript functions with two parameters: a state object and an action object. The state object includes the current state that will be modified, and the action object describes the operations that will be performed in the current state.

It returns the updated state object, which is then applied to the original state object, and the component is rerendered to show the latest changes, just like in the useState hook.

function reducer(state, action) {
  // Update the state

  return state;
}Code language: JavaScript (javascript)

You can place all of your state logic from different functions here. It simplifies your component and reduces complexity by separating state logic into its own function.

Now, rather than invoking the handler functions, we’ll now dispatch an action. The dispatched action is sent to the reducer function along with the current state, which modifies the state based on the action and returns the updated state.

Let’s look at what these actions are.

What are Actions?

Actions are JavaScript objects that describe the type of operation that the reducer must perform.

It is made up of two major parts: a type and a payload. The type determines the operation to be done, and the payload is the data necessary for the operation.

const action = {
  type: 'ADD_TODO',
  payload: 'Learn about React Reducers',
};
Code language: JavaScript (javascript)

A reducer function takes these actions as an argument, extracts the required values from it, performs the operation on the state object, and returns the new state object.

However, you do not call the reducer function directly and supply it with the state object and the action. To use the reducer, we dispatch an action.

But what do dispatching actions mean?

When you dispatch an action, you execute a special function called dispatch with the action object. The dispatch functions then call reducer with all of the necessary arguments and update the state.

It also makes other optimizations, such as batching the dispatch request, etc., to improve overall speed.

Let us now look at how we may get this dispatch function and learn how to dispatch events.

How to Dispatch Actions?

We’ve seen what dispatching an action means. But in order to dispatch an action, we need a dispatch function.

How do you get the dispatch function?

Here comes our useReducer hook. It takes a reducer function and the initial state as arguments and returns to us the state variable and the dispatch function.

const [state, dispatch] = useReducer(reducer, initialState);
Code language: JavaScript (javascript)

We can use this dispatch function to dispatch an action as below:

dispatch({
  type: 'ADD_TODO',
  payload: 'Learn about React Reducers',
});Code language: CSS (css)

It is not necessary to pass your data on the payload property; instead, you may also do something like this:

dispatch({
  type: 'ADD_TODO',
  todo: 'Learn about React Reducers',
});Code language: CSS (css)

Nonetheless, it is better to keep data on the payload property in order to separate the action type from the data.

Let’s go on and see how we may handle these actions within our reducer function.

How to handle Actions inside a Reducer function?

We’ve already discussed reducers. They take the old state and the action as arguments, make some changes to the state, and then return the updated state.

The action is used to determine which changes to make to the state. The action.type property on the action object is used to determine the type of modification to perform.

function reducer(state, action) {
  if (action.type === 'ADD_TODO') {
    // Update the state
    // ...

    return updatedState;
  }

  return state; // If no condition match
}
Code language: JavaScript (javascript)

Keep in mind that you must return something from the reducer. If no actions were found, return the default state. Otherwise, if nothing was returned, the current state value will be set to undefined.

We may also need to add/remove/update some data within the state by using a provided value. The action.payload property is used to obtain this data.

const updatedState = [...state, action.payload];Code language: JavaScript (javascript)

The reducer function uses these values to make the updates to the state object. After updating the state, it returns the new state. The returned value is then set as the new state. Our useReducer hook does everything behind the scenes, so you don’t have to worry about it.

After the state is updated, your component will render to show the most recent changes to the state.

Creating a TodoList app using the useReducer hook

Now that we’ve covered the fundamentals, let’s dive in and put our knowledge to the test by creating a simple TodoList app with the useReducer hook.

We’ll go step by step so you have a good grasp of the typical procedure followed while handling state management using the useReducer hook.

Step-1: Creating a Form

Let’s make a TodoList component with an input field for the Todo text, a button for adding the Todo, and a list of tasks. I’ve hardcoded a few list items so you can see how it will look.

import React from 'react';
import './App.css';

function App() {
  return (
    <main>
      <input type='text' />
      <button>Add ToDo</button>
      <ul>
        <li>
          <p>Read a Book</p>
          <button>Mark as completed</button>
        </li>
        <li>
          <p>Resume web development course on Codedamn</p>
          <button>Mark as completed</button>
        </li>
      </ul>
    </main>
  );
}

export default App;
Code language: JavaScript (javascript)
Hardcoded ToDo list
Hardcoded ToDo list

For a completed Todo, we’ll mark them with a strikethrough. We do this using the text-decoration CSS property.

.completed {
  text-decoration: line-through;
}Code language: CSS (css)
Todos when marked as completed
Todos when marked as completed

Step-2: Create Actions

We just need three actions for a simple Todo: one to add a Todo, another to mark the to-do as complete, and one more to unmark the todo.

To get the dispatch function, let’s create an empty reducer function and pass it to the useReducer hook. Because there are no Todos at the start, the initial state is an empty array.

function reducer(state, action) {
  // Will add stuff later
}Code language: JavaScript (javascript)
const [state, dispatch] = useReducer(reducer, []);
Code language: JavaScript (javascript)

Now that we have the dispatch function, we can dispatch actions with button clicks. We are currently using a total of three actions: ADD_TODO, MARK_TODO_AS_COMPLETE, and MARK_TODO_AS_INCOMPLETE, to add a Todo, mark the Todo as completed, and unmark it.

const handleAddToDo = (e) => {
  const value = document.querySelector('input').value;
  dispatch({ type: 'ADD_TODO', payload: value });
};

const handleCompleted = (id) => {
  dispatch({ type: 'MARK_TODO_AS_COMPLETE', payload: id });
};

const handleUndo = (id) => {
  dispatch({ type: 'MARK_TODO_AS_INCOMPLETE', payload: id });
};Code language: JavaScript (javascript)

Also, add the onClick event listener on the “Add ToDo” button.

<button onClick={handleAddToDo}>Add ToDo</button>
Code language: JavaScript (javascript)

Additionally, let’s change our JSX to show the Todo list based on our state variable rather than some hardcoded values.

<ul>
  {state.map((todo) => (
    <li key={todo.id}>
      <p className={todo.completed ? 'completed' : null}>{todo.text}</p>
      {!todo.completed ? (
        <button onClick={() => handleCompleted(todo.id)}>
          Mark as completed
        </button>
      ) : (
        <button onClick={() => handleUndo(todo.id)}>Undo</button>
      )}
    </li>
  ))}
</ul>Code language: JavaScript (javascript)

This will result in an empty list with an input and the “Add ToDo” button.

Empty Todo list
Empty Todo list

However, because we are not handling the actions within our reducer, the “Add ToDo” button will not work right now.

Step-3: Handling Actions inside the Reducer

Here comes the exciting part: we’ll be incorporating state logic into our reducer function. We’ll start by using the type property on the action object to determine the type of action.

function reducer(state, action) {
  if (action.type === 'SOME_ACTION') {
    // Do something
  }

  return state; // if no condition match
}Code language: JavaScript (javascript)

We’ll check for three action types: ADD_TODO, MARK_TODO_AS_COMPLETE, and MARK_TODO_AS_INCOMPLETE, but if there were more, we’d check them all.

function reducer(state, action) {
  if (action.type === 'ADD_TODO') {
    // Do something
  }

  if (action.type === 'MARK_TODO_AS_COMPLETE') {
    // Do something
  }

  if (action.type === 'MARK_TODO_AS_INCOMPLETE') {
    // Do something
  }
  
  return state; // If no conditions match
}Code language: JavaScript (javascript)

Now, for the ADD_TODO action, we’ll make a new array containing the current item and return it as the new state. We’re using the current timestamp as the ID for the Todos.

if (action.type === 'ADD_TODO') {
  const newState = [...state, { id: Date.now(), text: action.payload }];
  return newState;
}Code language: JavaScript (javascript)

It is crucial to keep in mind that we cannot simply mutate the state variable supplied to the reducer function; we must return a new state from it.

Similarly, for the MARK_TODO_AS_COMPLETED action, we’ll find the ToDo using the ID provided, mark it as completed, and then return the updated state.

if (action.type === 'MARK_TODO_AS_COMPLETE') {
  const newState = state.map((todo) => {
    if (todo.id === action.payload) {
      return { ...todo, completed: true };
    }
    return todo;
  });
  return newState;
}Code language: JavaScript (javascript)

And just the opposite for the MARK_TODO_AS_INCOMPLETE action:

if (action.type === 'MARK_TODO_AS_INCOMPLETE') {
  const newState = state.map((todo) => {
    if (todo.id === action.payload) {
      return { ...todo, completed: false };
    }
    return todo;
  });
  return newState;
}Code language: JavaScript (javascript)

With this, we’re all set to test our TodoList mini-project.

Working TodoList app using Reducers and the useReducer hook
Working TodoList app using Reducers and the useReducer hook

You may try out the app using the Codedamn Playground embedded below:

The app is functioning correctly. All of this without cluttering our component with a bunch of functions that handle state logic.

Applications of React Reducers and the useReducer hook

As said before, React Reducers and their application are essential for any React developer to understand. Not only is it a basic application, but it can manage very complex state logic without sacrificing performance, and as a result, it outperforms the state.

The most common use of Reducers is to replace the useState hook when dealing with complex state logic. Furthermore, if you’ve used Redux, you’re aware that it follows the same Action-Reducer pattern that we just discussed.

Therefore, we may use Reducers and the useReducer hook to replace these libraries. We can even create a tiny Redux for ourselves by combining it with Contexts and the useContext hook 🤯

Summary

The most important aspect of developing React web apps is state management, but things can become complicated as the application grows.

We usually use the useState hook for managing the state. But, as the state becomes more complex, handling state logic becomes more challenging as we need to keep track of various handler functions.

This is when React Reducers and the useReducer hook comes into play. They help in extracting the state logic from the component into a single function known as the reducer function. This reducer function includes all of the necessary state logic.

With reducers in place, we dispatch actions with dispatch functions instead of calling handlers. This pattern is far more efficient than the useState hook in both performance and code maintainability.

To manage a state, popular state management libraries like Redux employ the same Action-Reducer pattern. We can combine Reducers and Contexts to create our own little Redux library. 🤯

I’ll be back with more interesting React hooks in this special series – “Mastering React Hooks”, so stay tuned 🚀

This article hopefully provided you with some new information. Share it with your friends if you enjoy it. Also, please provide your feedback in the comments section.

Thank you so much for reading 😄

Sharing is caring

Did you like what Varun Tiwari wrote? Thank them for their work by sharing it on social media.

0/10000

No comments so far