Simplifying state management with useReducer hook

vivekalhat

Vivek Alhat

Posted on April 23, 2023

Simplifying state management with useReducer hook

State management is a fundamental aspect of React. React provides different native ways to manage and maintain state. One of the most commonly used way to manage a state is useState hook.

When you start your journey of learning hooks in React, useState will probably be your first destination. The useState hook provides a simple API to manage a component state. Below is an example of useState hook.

const [count, setCount] = useState(0)
Enter fullscreen mode Exit fullscreen mode

The useState hook returns two variables:

  1. count - an actual state
  2. setCount - a state updater function

A React component generally contains the logic that returns some JSX i.e. the logic to render UI. When you add state management to the component then it contains both logic for rendering a UI and managing the state. When a component gets big, it becomes quite challenging to maintain both state management and UI rendering logic. Although, this works totally fine still a React component should separate the UI and state management logic.

Let’s look at the below example:

import { useState } from "react";
import { v4 as uuidv4 } from "uuid";

const ListState = () => {
  const defaultTodo = [
    { id: uuidv4(), title: "\"Write a new blog\", isComplete: false },"
    { id: uuidv4(), title: "\"Read two pages of Rework\", isComplete: true },"
    { id: uuidv4(), title: "\"Organize playlist\", isComplete: false },"
  ];

  const [todo, setTodo] = useState(defaultTodo);
  const [item, setItem] = useState("");

  // Marks a given todo as complete or incomplete
  const handleTodoChange = (id) => {
    const updatedTodos = todo.map((item) => {
      if (item.id === id) {
        return { ...item, isComplete: !item.isComplete };
      }
      return item;
    });
    setTodo(updatedTodos);
  };

  // Adds a new todo item to the list
  const handleAddItem = (e) => {
    e.preventDefault();
    const updatedTodos = [
      ...todo,
      { id: uuidv4(), title: "item, isComplete: false },"
    ];
    setTodo(updatedTodos);
    setItem("");
  };

  // Removes todo item from the list based on given ID
  const handleDeleteItem = (id) => {
    const updatedTodos = todo.filter((item) => item.id !== id);
    setTodo(updatedTodos);
  };

  return (
    <div className="container">
      <div className="new-todo">
        <input
          placeholder="Add New Item"
          value={item}
          onChange={(e) => setItem(e.target.value)}
          className="todo-input"
        />
        <button onClick={handleAddItem} className="add-todo">
          Add
        </button>
      </div>
      <ul className="todo-list">
        {todo.length > 0 ? (
          todo.map((item) => (
            <li key={item.id} className="list-item">
              <input
                type="checkbox"
                checked={item.isComplete}
                onChange={() => handleTodoChange(item.id)}
              />
              <p>{item.title}</p>
              <button
                onClick={() => handleDeleteItem(item.id)}
                className="delete-todo"
              >
                Delete
              </button>
            </li>
          ))
        ) : (
          <p>List is empty</p>
        )}
      </ul>
    </div>
  );
};

export default ListState;
Enter fullscreen mode Exit fullscreen mode

The above code renders a simple Todo list where a user can do the following things:

  1. Add a new todo item
  2. Delete a todo item
  3. Mark a todo as complete/incomplete.

The above component contains both the logic for handling state and rendering JSX.

React provides a native hook called useReducer using which we can separate the logic for UI and state management. A useReducer hook is an alternative to useState hook.


What is useReducer?

The useReducer hook allows you to add state management to your component similar to useState hook. The only difference is that it does this by using a reducer pattern. The API for useReducer hook is as follows:

const [state, dispatch] = useReducer(reducerFn, initialState)
Enter fullscreen mode Exit fullscreen mode

The useReducer hook takes two parameters:

  1. A reducer function that contains the logic for state management.
  2. An initial state

The useReducer hook returns two variables:

  1. The current state
  2. A dispatch function which when called invokes the reducer function to update the state.

Let’s learn to use this hook by refactoring the above Todo list component.

import { useState, useReducer } from "react";
import { ListReducerFn } from "../utils/functions";
import { defaultTodo } from "../utils/defaults";

const ListReducer = () => {
  const [todo, dispatch] = useReducer(ListReducerFn, defaultTodo);
  const [item, setItem] = useState("");

  // Marks a given todo as complete or incomplete
  const handleTodoChange = (id) =>
    dispatch({ type: "UPDATE", payload: { id } });

  // Adds a new todo item to the list
  const handleAddItem = (e) => {
    e.preventDefault();
    dispatch({ type: "ADD", payload: { item } });
    setItem("");
  };

  // Removes todo item from the list based on given ID
  const handleDeleteItem = (id) =>
    dispatch({ type: "DELETE", payload: { id } });

  return (
    <div className="container">
      <div className="new-todo">
        <input
          placeholder="Add New Item"
          value={item}
          onChange={(e) => setItem(e.target.value)}
          className="todo-input"
        />
        <button onClick={handleAddItem} className="add-todo">
          Add
        </button>
      </div>
      <ul className="todo-list">
        {todo.length > 0 ? (
          todo.map((item) => (
            <li key={item.id} className="list-item">
              <input
                type="checkbox"
                checked={item.isComplete}
                onChange={() => handleTodoChange(item.id)}
              />
              <p>{item.title}</p>
              <button
                onClick={() => handleDeleteItem(item.id)}
                className="delete-todo"
              >
                Delete
              </button>
            </li>
          ))
        ) : (
          <p>List is empty</p>
        )}
      </ul>
    </div>
  );
};

export default ListReducer;
Enter fullscreen mode Exit fullscreen mode
// utils/defaults.js

import { v4 as uuidv4 } from "uuid";

export const defaultTodo = [
  { id: uuidv4(), title: "Write a new blog", isComplete: false },
  { id: uuidv4(), title: "Read two pages of Rework", isComplete: true },
  { id: uuidv4(), title: "Organize playlist", isComplete: false },
];
Enter fullscreen mode Exit fullscreen mode
// utils/functions.js

import { v4 as uuidv4 } from "uuid";

export const ListReducerFn = (state, action) => {
  switch (action.type) {
    case "ADD": {
      const updatedTodos = [
        ...state,
        { id: uuidv4(), title: action.payload.item, isComplete: false },
      ];
      return updatedTodos;
    }
    case "DELETE": {
      const updatedTodos = state.filter(
        (item) => item.id !== action.payload.id
      );
      return updatedTodos;
    }
    case "UPDATE": {
      const updatedTodos = state.map((item) => {
        if (item.id === action.payload.id) {
          return { ...item, isComplete: !item.isComplete };
        }
        return item;
      });
      return updatedTodos;
    }
    default: {
      throw new Error(`Unsupported action: ${action.type}`);
    }
  }
};
Enter fullscreen mode Exit fullscreen mode

As you can see, the JSX for both versions of Todo list component is same. The only difference is in the logic for handling state management. Let’s discuss the differences between the Todo list component that uses useState and useReducer hooks.

Here is a function to add a new todo list item written using useState logic.

 // Adds a new todo item to the list (useState version)
  const handleAddItem = (e) => {
    e.preventDefault();
    const updatedTodos = [
      ...todo,
      { id: uuidv4(), title: item, isComplete: false },
    ];
    setTodo(updatedTodos);
    setItem("");
  };
Enter fullscreen mode Exit fullscreen mode

In the above code, handleAddItem function is used for adding a new todo item to the list. We are creating a new array of updated todo list items and setting the state with updated todo list that also includes newly added item. We are updating the state directly from the function itself.

Let’s look at the useReducer version of the same function.

// Adds a new todo item to the list (useReducer version)
  const handleAddItem = (e) => {
    e.preventDefault();
    dispatch({ type: "ADD", payload: { item } });
    setItem("");
  };
Enter fullscreen mode Exit fullscreen mode

The above function does the same thing i.e. it adds a new todo item to the list. In this scenario, the state management logic is separated and we are not directly updating the state from the function. Instead, here we are using dispatch function provided by useReducer to dispatch an action. Whenever an action is dispatched, the reducer function is called to modify and return the new state based on the type of action.

What is dispatch function?

A dispatch is a special function that dispatches an action object. It basically acts as a request to update the state. The dispatch function takes an action object as a parameter. You can pass anything to dispatch function but generally you should pass only the useful information to update the state.

Here is a sample action object.

const action = {
    type: "ADD",
    payload: {
        todo: "Write a new blog article"
    }
}
Enter fullscreen mode Exit fullscreen mode

The action object should generally contain following two properties:

  1. type - It basically tells reducer what type of action has happened. (eg. ‘ADD’, ‘DELETE’ etc.)
  2. payload (optional) - An optional property with some additional information required to update the state.

The dispatch function invokes the reducer function to update and return the new state. The logic to update the state is done inside the reducer function instead of a React component. In this way, the state management can be separated from UI rendering logic.

What is a reducer function?

A reducer function handles all the logic of how a state should be modified. It takes two parameters:

  1. A current state
  2. An action

Following is a sample reducer function that handles the logic of updating todo list state.

export const ListReducerFn = (state, action) => {
  switch (action.type) {
    case "ADD": {
      const updatedTodos = [
        ...state,
        { id: uuidv4(), title: action.payload.item, isComplete: false },
      ];
      return updatedTodos;
    }
    case "DELETE": {
      const updatedTodos = state.filter(
        (item) => item.id !== action.payload.id
      );
      return updatedTodos;
    }
    case "UPDATE": {
      const updatedTodos = state.map((item) => {
        if (item.id === action.payload.id) {
          return { ...item, isComplete: !item.isComplete };
        }
        return item;
      });
      return updatedTodos;
    }
    default: {
      throw new Error(`Unsupported action: ${action.type}`);
    }
  }
};
Enter fullscreen mode Exit fullscreen mode

Whenever an action is dispatched from a component, the reducer function is invoked to update and return the new state.

TL;DR

The useState and useReducer hooks allow you to add state management to the React component. The useReducer hook provides more flexibility as it allows you to separate UI rendering and state management logic.
You can check the code for above sample here.

💖 💪 🙅 🚩
vivekalhat
Vivek Alhat

Posted on April 23, 2023

Join Our Newsletter. No Spam, Only the good stuff.

Sign up to receive the latest update from our blog.

Related