Simplifying state management with useReducer hook
Tags: react, beginners, javascript, programming
Published: 2023-04-23 16:26:42

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.

```tsx
const [count, setCount] = useState(0)
```

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:

```tsx
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;
```

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:

```tsx
const [state, dispatch] = useReducer(reducerFn, initialState)
```

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.

```tsx
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;
```

```tsx
// 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 },
];
```

```tsx
// 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}`);
    }
  }
};
```

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.

```tsx
 // 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("");
  };
```

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.

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

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.

```tsx
const action = {
	type: "ADD",
	payload: {
		todo: "Write a new blog article"
	}
}
```

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.

```tsx
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}`);
    }
  }
};
```

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](https://github.com/VivekAlhat/Hooks-Playground).