Exploring the Efficiencies of useReducer in React: A Comparative Analysis of State Management (part-1)

This article will give a general understanding of how to implement useReducer in React.

Exploring the Efficiencies of useReducer in React: A Comparative Analysis of State Management (part-1)

Introduction

I previously favored useState for managing state in React applications, however, I have recently implemented useReducer and have found it to be a more efficient method in comparison to the traditional useState. In specific situations, useReducer has proven to be the optimal solution for managing state. One of the key benefits of using useReducer is its effectiveness in managing global state within large React applications. This is particularly useful when combined with the useContext hook.

In Part 2, I will demonstrate the benefits of combining useReducer and useContext for managing state in React applications, as well as provide a comparison with useState.

This article aims to provide a comprehensive overview of the use of useReducer in React, including a comparison of its functionality and benefits in relation to the traditional useState hook. So let's get hooked, shall we?

Before diving deeper into the topic, it is crucial to understand the concept of a reducer.

What is reducer?

Reducer is not a concept specific to React, rather it is a concept of JavaScript. To understand it, let's explore the built-in method in JavaScript called Array.prototype.reduce(). Its purpose is to reduce an array into a single value by taking a reducer callback function and an initial value as arguments. Let's see the code below:

//define the reducer function
const reducer = (accumulator, currentValue) => accumulator + currentValue;

//Now use this reducer as a callback in Array.prototype.reduce()
const array1 = [1, 2, 3, 4];
let initial_value = 0;
const sum = array1.reduce(reducer, initial_value);
console.log(sum); //10
initial_value = 5;
const sum2 = array1.reduce(reducer, initial_value);
console.log(sum2); //15

See mdn web docs for better understanding of Array.prototype.reduce().

Once you have a grasp on the concept of a reducer, you are ready to proceed with understanding the use of useReducer in React.

What is useReducer?

It is an important hook in React for state management, it allows you to add a reducer function to your component. Both the Array.prototype.reduce() method and useReducer have similar purposes, which is to reduce a collection of values into a single value by taking a reducer function and an initial state as arguments and applying the reducer function to each value in the collection to produce the final reduced value.

Now, let's explore the similarities and differences between the JavaScript's reduce() method and React's useReducer hook :

reduceuseReducer
array.reduce(reducer, initialValue)useReducer(reducer, initialState)
reducer(accumulator, currentValue)reducer(currentState, action)
returns a single valuereturns an array = [state, dispatch]

Let's have a look at the implementation of useReducer in a simple react component :

import { useReducer } from 'react'; 
const initialState = 0; 
const reducer = (state, action) => { 
  switch (action) { 
    case 'increment':
      return state + 1; 
    case 'decrement': 
      return state - 1; 
    default: 
      return state; 
  } 
} 

const Counter = () => { 
  const [count, dispatch] = useReducer(reducer, initialState);
    return ( 
      <> 
        <p>Count: {count}</p> 
          <button onClick={() => dispatch('increment')}>
            +
          </button>
          <button onClick={() => dispatch('decrement')}>
            -
          </button> 
      </> 
    ); 
};

In this example, we have defined a reducer function that takes the current state and an action as arguments, and returns a new state based on the action type. We have also defined an initial state 0.

We then use the useReducer hook to create a state and a dispatch function that we can use to update the state. We pass our reducer function and initial state to useReducer, and it returns the current state and the dispatch function.

We use the dispatch function to send actions to our reducer function whenever the user clicks the + or - buttons. The reducer function then updates the state based on the action, and the component re-renders with the new state. Simple!

It is understandable to wonder about the advantages of using useReducer over useState. In situations where the complexity of logic is uncertain, it may be initially simpler to utilize a few instances of useState. However, as the logic and states expand, the codebase can become increasingly complex and difficult to read. In these scenarios, utilizing useReducer can prove to be a more efficient and effective solution for managing state and logic within the application.

When to choose useReducer?

So, when is it appropriate to use useState and when should you consider using useReducer? Here are some general guidelines to help you make your decision:

  • If you only have a single piece of state and the logic for updating it is simple, useState is probably the way to go.

  • If you have multiple pieces of state that are related and their updates depend on one another, useReducer might be a better fit.

  • If you have state that needs to be updated based on an action or event (such as a form submission or a button click), useReducer is a good choice because it allows you to define a reducer function that handles the various actions.

useReducer is similar to the useState hook in that it allows you to manage the state of a component, but it provides an additional layer of abstraction by allowing you to move the state update logic into a single, separate function outside of your component. This can make your code more readable and easier to reason about, particularly in situations where the state logic becomes complex. It's important to note that while useReducer can provide more powerful and flexible state management, it also requires a deeper understanding of how it works.

The combination of useReducer and useContext is an effective approach when working with global states across multiple components in a React application. The useContext hook allows for easy access to the global state, while useReducer provides a clear and organized method for managing and updating that state. It is worth noting that the use of the Context API is a topic that warrants further discussion and I will be happy to provide additional insights on the matter in a future post (part-2). Now, let's move on to exploring a demonstration of the use of useReducer in a slightly more complex state logic, one that involves more than just a primitive value as we have previously used.

Here is an example of using the useReducer hook to handle a form with multiple inputs.

reducer.js :

export const initialState = {
  values: {
    name: '',
    email: '',
    password: '',
    confirmPassword: '',
  },
  errors: {
    name: '',
    email: '',
    password: '',
    confirmPassword: '',
  },
  isSubmitting: false,
};

//reducer function. This is where we will have our state logic. 
export const formReducer = (state, action) => {
  switch (action.type) {
    case 'updateValue':
      return {
        ...state,
        values: {
          ...state.values,
          [action.field]: action.value,
        },
      };
    case 'validate':
      const errors = {};
      if (!state.values.name) {
        errors.name = 'Name is required';
      }
      if (!state.values.email) {
        errors.email = 'Email is required';
      } 
      if (!state.values.password) {
        errors.password = 'Password is required';
      } 
      return {
        ...state,
        errors,
      };
    case 'submit':
      return {
        ...state,
        isSubmitting: true,
      };
    case 'success':
      return initialState;
    case 'error':
      return {
        ...state,
        isSubmitting: false,
      };
    default:
      return state;
  }
};

The initialState variable defines the initial values for the form inputs, as well as the errors and the submitting status of the form.

The formReducer function takes in the current state and an action, and returns the new state based on the action type. The actions that can be dispatched to the reducer are:

  • updateValue: updates the value of a specific field in the form

  • validate: performs validation checks on all the fields in the form and updates the error state accordingly

  • submit: sets the form's submitting status to true

  • success: resets the form to its initial state

  • error: sets the form's submitting status to false

Form.jsx :

import { useReducer } from 'react';
import { formReducer, initialState } from 'reducer';

const Form = () => {
  const [state, dispatch] = useReducer(formReducer, initialState);

  const handleChange = (event) => {
    dispatch({
      type: 'updateValue',
      field: event.target.name,
      value: event.target.value,
    });
  };

  const handleSubmit = (event) => {
    event.preventDefault();
    dispatch({ type: 'validate' });
    if (!Object.values(state.errors).some((error) => error)) {
      dispatch({ type: 'submit' });
      // Make API call to submit form
      // On success: dispatch({ type: 'success' });
      // On error: dispatch({ type: 'error' });
    }
  }

  return (
    <form onSubmit={handleSubmit}>
      <div>
        <label htmlFor="name">Name:</label>
        <input
          type="text"
          id="name"
          name="name"
          value={state.values.name}
          onChange={handleChange}
        />
        {state.errors.name && <p>{state.errors.name}</p>}
      </div>
      <div>
        <label htmlFor="email">Email:</label>
        <input
          type="email"
          id="email"
          name="email"
          value={state.values.email}
          onChange={handleChange}
        />
        {state.errors.email && <p>{state.errors.email}</p>}
      </div>
      <div>
        <label htmlFor="password">Password:</label>
        <input
          type="password"
          id="password"
          name="password"
          value={state.values.password}
          onChange={handleChange}
        />
        {state.errors.password && <p>{state.errors.password}</p>}
      </div>
      <div>
        <label htmlFor="confirmPassword">Confirm Password:</label>
        <input
          type="password"
          id="confirmPassword"
          name="confirmPassword"
          value={state.values.confirmPassword}
          onChange={handleChange}
        />
        {state.errors.confirmPassword && <p>{state.errors.confirmPassword}</p>}
      </div>
      <button type="submit" disabled={state.isSubmitting}>
        Submit
      </button>
    </form>
  );
}

The Form component uses the useReducer hook to create a state and a dispatch function from the formReducer and the initialState. The component has a number of input fields that are bound to the state values and have their onChange event handlers set to the handleChange function, which dispatches the updateValue action to the reducer. The form also has a onSubmit event handler set to the handleSubmit function, which dispatches the validate action to the reducer, and then if there are no errors it will dispatch the submit action. After that, it uses the form data to make an API call and on success it dispatches the success action and on error it dispatches the error action.

Summary

The centralization of state logic within the reducer function allows for improved readability and maintainability of the codebase. As the development process progresses, new cases can easily be added to the reducer function without the need to import multiple instances of useState, thereby avoiding an unmanageable codebase.

Overall, the choice between useState and useReducer comes down to the complexity of your state and how you need to update it. Both useState and useReducer are useful tools in the React developer's toolkit, and learning when to use each one will help you build efficient and maintainable applications.

Conclusion

In conclusion, this article has provided an overview of the use of useReducer in React, including a comparison of its functionality and benefits in relation to the traditional useState hook. We have also discussed the concept of a reducer and its connection to useReducer. This is just the beginning of the journey, in the next part of this series we will dive deeper into the usage and implementation of useReducer in React Applications. We will examine the advantages of combining useReducer and useContext in managing state and how it differs from useState. Stay tuned!