The useReducer Hook: The Ultimate Guide (Build a Counter and a todo App)

The useReducer Hook_ The Ultimate Guide (Build a Counter and a todo App) _ MediaOne Singapore

The useReducer Hook: The Ultimate Guide (Build a Counter and a todo App)

YouTube video

React has so many hooks (15, to be precise) that it’s difficult to keep track of all of them. But one Hook stands out: `useReducer. The first hook people learn is useState, a state management hook that lets you manage states in functional components.

But let’s be honest, managing states can get a little messy with useState. That’s where `useReducer` steps in.

Managing States in React

A state is a snapshot of your application at any given moment. For example, adding a new task in a todo app will change the state. The app will have a +1 task. In other words, any interaction with a React app changes its state. 

In React, there are three ways to manage states:

  1. i) Using React Hooks (useState and useReducer) — For Functional Components
  2. ii) Using setState() method — For Class Components

iii) Using Redux — A State Management Library

Out of these three, useState is the simplest way to manage states in functional components. It’s a built-in hook that allows you to add stateful logic to functional components. But as your application grows and becomes more complex, managing states with useState can become cumbersome. That’s when you should consider using `useReducer`.

So, What Exactly is useReducer?

What Exactly is useReducer

get low cost monthly seo packages

In simple terms, useReducer is a React Hook that gives us more control over managing state updates. It’s a more advanced alternative to useState, allowing you to handle complex states in a more organized and efficient manner.

Here are the limitations of useState and How useReducer Overcomes Them:

  • With useState, you can only manage one state at a time. But with `useReducer`, you can manage multiple states in the same component.
  • useState requires props drilling to pass state values down to child components. Meaning, you cannot pass states between sibling components. But with `useReducer`, you can easily share state values among sibling components without props drilling.
  • useState does not allow for complex state updates. When using `useReducer`, you have more control over how your states are updated, making it easier to manage complicated state logic.
  • With useState, you must write a lot of repetitive code to handle similar states. But with `useReducer`, you can create reusable state updates.

In summary, use ‘useState’ only for minor state management tasks, like checkbox toggles, colour changes, and simple form inputs. For more complex state management, use `useReducer` instead.

Examples of When to Use useReducer

  • Creating a shopping cart for an e-commerce website.
  • Building a todo list application with multiple lists and tasks.
  • Managing form states with complicated validation logic.
  • Handling global state in large applications using context API.
  • Managing state for a game with different levels and scores.
  • Tracking multiple user inputs in a multi-step form.
  • Implementing undo/redo functionality in an app. 

In all these scenarios, `useReducer` proves to be more efficient in managing states compared to useState. Plus, it allows for better code organization and reusability. So why not give it a try? You might just fall in love with it.

The Syntax of useReducer

The syntax for `useReducer` is quite simple, but before we dive into it, let’s first understand the main concepts involved. 

  • Reducers: Reducers are pure functions that take in two arguments – the current state and an action object – and return a new state.
  • Actions: Actions are objects with a type property that specifies the type of action being performed. They can also contain additional data to be used in the reducer function.
  • Dispatch: Dispatch is a function provided by useReducer that takes an action object as its argument and triggers the state update process.

Now, let’s look at the syntax for `useReducer`:

const [state, dispatch] = useReducer(reducer, initialState); 

  • State: This is the current state returned by the reducer function.
  • Dispatch: This is a function used to dispatch actions and trigger state updates.
  • Reducer: This is the reducer function that takes in two arguments – the current state and an action object – and returns a new state.
  • initialState: This is the initial value for the state.

Prerequisites:

To understand this guide, here are some of the things you should know:

  • A good knowledge of JavaScript, particularly the array.reduce() function.
  • Familiarity with React Hooks, particularly useState.

Don’t worry if you’re not familiar with these concepts. By the end of this guide, you’ll have a good understanding of `useReducer` and how to use it in your React projects.

How does useReducer Work?

Now that we understand the syntax, let’s see how `useReducer` actually works. 

To understand how the hook works, we’ll have to cycle back and revisit JavaScript’s Array.prototype.reduce() method. `useReducer` is built on top of this function, and to understand how it works, it helps to first understand the array.reduce method.

The reduce method works on an array to reduce it to a single value or object. It takes in a callback function (also known as the reducer function) and an optional initial value.

The syntax for array.reduce is:

array.reduce(reducer, initialValue);

The reducer function takes two arguments: accumulator (which keeps track of the reduced value) and currentValue (which represents each element of the array). The reducer function performs some operation on these two values and returns the updated accumulator.

Let’s look at an example:

Suppose we have an array of numbers: [1, 2, 3, 4].

We use `reduce` to sum up all the elements in the array using a reducer function that adds each element to the accumulator and returns it.

The code would look like this:

const numbers = [1, 2, 3, 4];

const sum = numbers.reduce((accumulator, currentValue) => accumulator + currentValue)

console.log(sum) || returns 10

engaging the top social media agency in singapore

Here’s what happens:

  • The reducer function takes in two arguments (accumulator and currentValue).
  • Initially, the accumulator is set to the initialValue (if provided) or the first element of the array. In this case, the accumulator is set to 1 since the initial value isn’t provided.
  • The reducer function adds 1 (accumulator) and 2 (currentValue) and returns the updated accumulator, which is now set to 3.
  • Next, the new accumulator (3) is added to the next element in the array (3), giving us a new accumulator of 6.
  • This process continues until all elements in the array are exhausted, and the final accumulator is returned as the result.
ALSO READ
Top Social Media Platforms In Singapore (Trends, Strategies, Case Studies)

Now, let’s try the reduce method with an initial value of 10:

const numbers = [1, 2, 3, 4];

const initialValue = 10;

const sum = numbers.reduce((accumulator, currentValue) => accumulator + currentValue, initialValue)

console.log(sum) || returns 20

Here’s how this works: the accumulator is set to the initial value of 10, and each element in the array is added to it, giving us a final sum of 20.

We can achieve the same result using useReducer in React:

The useReducer hook in React works similarly to the reduce method. It takes in a reducer function and an initial state value, and returns a new state and a dispatch function that is used to update the state.

But here’s where it gets interesting — instead of working with arrays, useReducer works with objects and allows you to update specific state fields without mutating the entire object.

Building a Counter App

Now that we understand the basics, let’s build our first counter app using useReducer in React. We will be creating a simple app that allows the user to increment, decrement, and reset the counter value.

First, let’s create the app’s Increment, decrement, and reset buttons. 

import React, {useReducer } from ‘react’;

export default function App() {

  return (

    <div className=”App”>

      <button>Increment</button>

      <button>Decrement</button>

      <button>Reset</button>

    </div>);

  }

Next, let’s write use reducer

const [state, dispatch] = useReducer(reducer, initialState);

import React, { useReducer } from ‘react’;

export default function App() {

const [state, dispatch] = useReducer(reducer, initialState);

  return (

    <div className=”App”>

      <button>Increment</button>

      <button>Decrement</button>

      <button>Reset</button>

    </div>);

  }

Here, we are initializing our state and dispatch function using useReducer.

The state variable will hold our current counter value, while the dispatch function will allow us to update it.

Now, let’s add some functionality to our buttons:

<button onClick={() => dispatch({ type: ‘INCREMENT’, payload: 1 })}>Increment</button>

<button onClick={() => dispatch({ type: ‘DECREMENT’, payload: 1 })}>Decrement</button>

<button onClick={() => dispatch({ type: ‘RESET’ })}>Reset</button>

We are using the dispatch function to update our state based on the action type. For Increment and decrement, we are passing in a payload of 1 to add or subtract from the current counter value.

A payload is a piece of data that is passed along with an action to the reducer. It’s like a note attached to the action, telling the reducer what specific data to use when updating the state.

import React, { useReducer } from ‘react’;

export default function App() {

const [state, dispatch] = useReducer(reducer, initialState);

  return (

    <div className=”App”>

     <button onClick={() => dispatch({ type: ‘INCREMENT’, payload: 1 })}>Increment</button>

<button onClick={() => dispatch({ type: ‘DECREMENT’, payload: 1 })}>Decrement</button>

<button onClick={() => dispatch({ type: ‘RESET’ })}>Reset</button>

    </div>);

  }

Next, we need to set our initial state and create our reducer function:

const initialState = 0;

function reducer(state, action) {

  switch (action.type) {

    case ‘INCREMENT’:

      return state + action.payload;

    case ‘DECREMENT’:

      return state – action.payload;

    case ‘RESET’:

      return initialState;

    default:

      throw new Error();

  }

Here, we are setting our initial state to 0 and creating a reducer function that takes in the current state and action as parameters.

The switch statement allows us to specify different cases based on the action type. For each case, we return the updated state based on the current counter value and payload.

Now, let’s see how this all comes together by creating a simple counter app using the useReducer Hook:

import ./styles.css“;

import {useReducer} from ‘react’

export default function App() {

  const initialState = 0;

function reducer(state, action) {

  switch (action.type) {

    case ‘INCREMENT’:

      return state + action.payload;

    case ‘DECREMENT’:

      return state action.payload;

    case ‘RESET’:

      return initialState;

   

    default:

      throw new Error();

  }

}

const [state, dispatch] = useReducer(reducer, initialState);

  // Output: state.sum = 11

  return (

    <div className=App“>

      Count: {state}

      <button onClick={() => dispatch({ type: ‘INCREMENT’, payload: 1 })}>Increment</button>

      <button onClick={() => dispatch({ type: ‘DECREMENT’, payload: 1 })}>Decrement</button>

      <button onClick={() => dispatch({ type: ‘RESET’ })}>Reset</button>

    </div>

  );

}

You can find the entire code here: https://codesandbox.io/s/friendly-currying-82v8hg?file=/src/App.js

That’s a lot of code for a simple counter app, but let’s break it down. 

  • First, we import the useReducer Hook from React and set our initial state to 0. 
  • Then, we define our reducer function, which takes the current state and action as parameters.
  • We use a switch statement inside the reducer function to specify different cases based on the action type. 
  • For each case, we return the updated state based on the current counter value and payload. The payload represents the amount by which we want to increment, decrement, or reset our counter.
  • Finally, we call the useReducer Hook and pass in our reducer function and initial state. That should return an array with the current state value and a dispatch function that allows us to update the state by passing in an action object.

Our return statement displays the current state value and three buttons that call dispatch with different action types and payloads when clicked.

website design banner

We can still separate our code into smaller components to make it more readable and manageable. For example, we could create a separate file for our reducer function and import it into our App component. If needed, this will allow us to reuse the same reducer function in other components.

A Real-World Example: Managing State for a Todo List

YouTube video

Let’s say we have a simple todo list application that allows users to add, delete, and mark tasks as completed. We can use `useReducer` to manage the state of this app more efficiently.

Let’s begin by writing some pseudocode: 

  • We need a form to add new todos and submit them.
  • The state will contain an array of todos, each with an id and text.
  • We also need buttons to mark a todo as completed or delete it from the list.

Next, we can follow similar steps as before:

ALSO READ
Understanding the New Twitter Rate Limit

Step 1: Let’s create the input field and button for adding new todos.

<form onSubmit={handleSubmit}>

  <input

    type=”text”

    placeholder=”Add todo…”

    value={todo}

    onChange={(e) => setTodo(e.target.value)}

  />

  <button>Add</button>

</form>

We’ve created a form with an input field and a button that calls the handleSubmit function when clicked. The handleSubmit function will add our new todo to the list of todos.

The onChange event listener updates the todo state value with the current value of the input field.  That is a controlled component, meaning the value of the input field is controlled by React.

Step 2: Let’s write our useReducer Hook and define our reducer function.

const [state, dispatch] = useReducer(reducer, initialState);

Step 3: Define todo

Use useState to define the todo state value.

const [todo, setTodo] = useState(“”);

get google ranking ad

We want to set a variable for the input field so we can access its value later on. We can do this by using the useState Hook.

Step 4: Let’s define Our Initial State

const initialState = {

  todos: [],

};

Step 5: define the handleSubmit function to add a new todo to our state.

const handleSubmit = (e) => {

 e.preventDefault();

 dispatch({ type: “ADD_TODO”, payload: todo });

 setTodo(“”);

};

This function prevents the default behaviour of form submission, which would cause the page to refresh.

 It then dispatches an action to our reducer, passing in a unique id generated by the uuidv4() function and the text of our todo. Finally, it resets the input field by setting the todo state value back to an empty string.

Step 6: let’s create buttons to mark todos as completed or delete them.

<button onClick={() => dispatch({ type: “TOGGLE_COMPLETE”, payload: todo.id })}>Mark as Completed</button>

<button onClick={() => dispatch({ type: “DELETE_TODO”, payload: todo.id })}>Delete</button>

These buttons call the dispatch function with different action types and payloads. The first button marks a todo as completed by passing in the todo’s id. The second button deletes a todo by passing in the todo’s id.

Step 7: let’s update our reducer to handle these new actions.

const reducer = (state, action) => {

    switch (action.type) {

      case “ADD_TODO”: {

        return { …state, todos: […state.todos, action.payload] };

      }

      case “TOGGLE_COMPLETE”: {

        return {

          …state,

          todos: state.todos.map((todo) =>

            todo.id === action.payload ? { …todo, completed: !todo.completed } : todo

          ),

        };

      }

      case “DELETE_TODO”: {

        return {

          …state,

          todos: state.todos.filter((todo) => todo.id !== action.payload),

        };

      }

      default:

        return state;

    }

  };

Our reducer now has three cases to handle the different actions. The first case adds a new todo to our state by spreading in the current state and adding the new todo from the action’s payload. 

The second case uses the map method to iterate through our state and update the completed property of a specific todo based on its ID. Finally, the third case uses the filter method to remove a todo from our state based on its ID.

Step 8: Now let’s map through our todo state and render each todo as a list item. 

<ul>

        {state.todos.map((todo) => (

          <li key={todo.id}>

            <input

              type=”checkbox”

              checked={todo.completed}

              onChange={() => dispatch({ type: “TOGGLE_COMPLETE”, payload: todo.id })}

            />

            {todo.title}

            <button onClick={() => dispatch({ type: “DELETE_TODO”, payload: todo.id })}>Delete</button>

          </li>

        ))}

      </ul>

 Remember to add our two buttons to our JSX: one for marking as complete and one for delete. When clicked, these buttons will dispatch the corresponding action to our reducer.

And voila! We now have a working todo list application using the useReducer Hook. But wait, there’s more!

Step 9: Let’s Test It Out

Now that we have our reducer setup and our state mapped to our JSX, let’s test out our todo list application.

Here’s the full code:

import React, { useState, useReducer } from ‘react’;

import “./styles.css”;

export default function App() {

  const [todo, setTodo] = useState(”);

  const initialState = {

    todos: [],

  };

  

  const reducer = (state, action) => {

    switch (action.type) {

      case “ADD_TODO”: {

        return { …state, todos: […state.todos, action.payload] };

      }

      case “TOGGLE_COMPLETE”: {

        return {

          …state,

          todos: state.todos.map((todo) =>

            todo.id === action.payload ? { …todo, completed: !todo.completed } : todo

          ),

        };

      }

      case “DELETE_TODO”: {

        return {

          …state,

          todos: state.todos.filter((todo) => todo.id !== action.payload),

        };

      }

      default:

        return state;

    }

  };

  const handleSubmit = (e) => {

    e.preventDefault();

    dispatch({ type: “ADD_TODO”, payload: { id: Date.now(), title: todo, completed: false } });

    setTodo(“”);

  };

  const [state, dispatch] = useReducer(reducer, initialState);

  return (

    <div className=”App”>

      <form onSubmit={handleSubmit}>

        <input

          type=”text”

          placeholder=”Add todo…”

          value={todo}

          onChange={(e) => setTodo(e.target.value)}

        />

        <button>Add</button>

      </form>

      <ul>

        {state.todos.map((todo) => (

          <li key={todo.id}>

            <input

              type=”checkbox”

              checked={todo.completed}

              onChange={() => dispatch({ type: “TOGGLE_COMPLETE”, payload: todo.id })}

            />

            {todo.title}

            <button onClick={() => dispatch({ type: “DELETE_TODO”, payload: todo.id })}>Delete</button>

          </li>

        ))}

      </ul>

    </div>

  );

You can find the code here: https://codesandbox.io/s/reducer-todolist-0pp9m?file=/src/App.js

Now, let’s break down this code and understand how the useReducer Hook works. 

The useReducer Hook is a built-in React Hook that allows you to manage complex state logic in your application. It takes in a reducer function and an initial state as arguments and returns the current state and a dispatch function.

The reducer function is responsible for updating the state based on different actions passed through the dispatch function. In our example, we have defined five different actions: ADD_TODO, TOGGLE_COMPLETE, and DELETE_TODO. When an action is dispatched, the reducer function checks for the type of action and performs the necessary state updates.

In our todo app, we are using the useState Hook to manage a single piece of state – the input value for adding new todos. The value is then passed to our handleSubmt function, which dispatches the ADD_TODO action and sends the input value as payload.

As we add new todos, they are stored in an array in our state. We then use the map method to render each todo item as a list element. The checkbox allows us to mark a todo as completed by dispatching the TOGGLE_COMPLETE action with the todo’s id as payload. The delete button dispatches the DELETE_TODO action and removes the todo from our state.

With useReducer, we can handle more complex logic and avoid having multiple useState Hooks for different pieces of state. It also allows us to separate our state management into a single reducer function, making it easier to maintain and debug.

About the Author

Tom Koh

Tom is the CEO and Principal Consultant of MediaOne, a leading digital marketing agency. He has consulted for MNCs like Canon, Maybank, Capitaland, SingTel, ST Engineering, WWF, Cambridge University, as well as Government organisations like Enterprise Singapore, Ministry of Law, National Galleries, NTUC, e2i, SingHealth. His articles are published and referenced in CNA, Straits Times, MoneyFM, Financial Times, Yahoo! Finance, Hubspot, Zendesk, CIO Advisor.

Share:

Search Engine Optimisation (SEO)

Search Engine Marketing (SEM)

Social Media

Technology

Branding

Business

Most viewed Articles

Other Similar Articles