The useReducer hook with examples

with useReducer hook, we can separate what a user wants to do and how the component gets it done, separate the state management from the render logic.

The useReducer hook with examples

In this article, we are going to learn and master the useReducer hook which is one of the complex React hooks; then break down step by step application of the useReducer hook using different examples.

In react we have mainly two types of hooks which are responsible for the state management they are:

  1. useState
  2. useReducer

The React.useState hook is the most popularly used among the pair, to learn more about the React.useState hook read React’s official documentation about Hooks here.

useReducer is an alternative to the useState hook, useState are used when the state is a primitive value, has a simple UI transition and logic is not complicated.

useReducer is preferred when the logic is complex and complicated, it helps achieve a better separation of concerns. useReducer lets you separate the state management from the render logic.

Simple Steps To Master useReducer

  1. What is a reducer
  2. How to use the useReducer hook in React
  3. useState vs useReducer
    • Converting a useState counter app to useReducer example
    • More examples on useReducer
  4. When to use the useReducer Hook
  5. When not to use the useReducer Hook
  6. Conclusion

Prerequisites

Before we go ahead you should have.

  • Basic knowledge of HTML, CSS, and JavaScript.
  • Basic understanding of ES6 features.
  • Basic understanding of how to make use of npm

What is a reducer

Every time I hear the term useReducer.The Javascript Array.prototype.reduce() method comes to mind.

Let's have a quick delve into the javascript array.reduce() method.

Here we have an array of numbers.

let numbers = [2, 4, 6];
let sum = numbers.reduce((previousValue, currentValue) => {
    return previousValue + currentValue;
});

console.log(sum);        //12

To calculate the total element of this array, we used the Array.prototype.reduce() method

//Syntax
array.reduce((accumulator, currentValue) => {
  //do the thing
  //return something
}, initialValue)

The reduce method Takes two arguments

  • First, a callback function, mostly referred to as the reducer. which have two arguments (accumulator, currentValue)
  • Next an Optional Initial value

The reduce() method calls the callback function for every array element; more like looping through the array

The reducer() returns a single value, which we get after executing the callback function over every array element.

How to use the useReducer hook in React.

The useReducer works very similarly to the Array.prototype.reduce(). It takes a reducer method and initialState as its argument.

The reducer method here is an action that will be executed in other to get a single value from the given state.

The work of the reducer is to reduce.

The value returned by the reducer could be a number, string, an array or even an object but most importantly it will always return a single value.

One difference between the Array.prototype.reduce() and the useReducer is; that the useReducer hook adds more functionality with the return of a dispatch (action).

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

Where

InitialState:

This is the first value of our state, it is the original state which our application is initialized with. And the default value of our state when the component gets mounted for the first time.

Reducer function:

The reducer function returns an object which is used to replace/change the current state, this occurs when an action is passed and state is replaced/changed based on the action passed.

The reducer function is a regular function, it takes two arguments: State and Action. And returns a new state based on the action passed

State is the application's default state. Which is the initial state we originally passed to the useReducer.

Action: is an object which contains the details of the action type; it is passed by the dispatch function.

Dispatch function:

The useReducer returns a dispatch function, which dispatches (passes) an action object to the reducer function.

The dispatch function is a special function which we can pass around to other components through props

Applying the useReducer

Like any other React hook we need to import the useReducer from react; this allows us to hook into the features of useReducer

import React, { useReducer } from 'react';

useReducer accepts a reducer function and initial state as arguments and returns an array [state, dispatch]. We set it up using destructuring assignment similar to the useState hook.

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

Next, declare the reducer function, called in the useReducer syntax above.

The reducer function takes in state and action as its arguments.

Where the state is the application's current state and action is passed by the dispatch function mentioned above.

function reducer(state, action) {
      // select action type
}

Now we can call the dispatch function in our component, in various sections of our component where we require some action to take place. It return the type of action requested (action.type)

dispatch({ type: "increment" });

useState vs useReducer

To get a basic grasp of the useReducer we are going to compare a simple counter application using the useState hook and convert it to use the useReducer hooks instead.

useState counter example

In the useState counter app example, we are using the React hook useState to store, manage and modify the application state.

Here the useState returns an array containing the current state value and the second argument (setState) for updating the values.

Using the useState, All logic and state management is done in the component. This leads to the component being too large in complex applications.

import { useState } from "react";


export default function App() {
  const [count,setCount] = useState(0);
  const [color, setColor] = useState("")

  function coloring() {
    let randomColor = Math.floor(Math.random()*16777215).toString(16);
    setColor("#"+randomColor);
  }

  return (
      <div className="App">
        <p style={{color}}>Counter: {count}</p>
        <button onClick={()=>setCount(count - 1)}>-</button>
        <button onClick={()=>setCount(count + 1)}>+</button>
        <button onClick={coloring}>color</button>
        </div>
  );
}

useReducer counter example

In this example, we are storing our initial state using the useReducer hook object initialState.

We can focus more on the actions type the user will take. With useReducer, we can separate what a user wants to do and how the component gets it done.

This is good for Separation of Concern, where we can separate the UI from the business logic. For example, we can create a container file containing all the logic (reducer function, initialState...) which would then be imported into the presentation component.

import { useReducer } from "react";

//reducer function
const counterReducer = (state, action) => {
  switch (action.type) {
    case "increment":
      return {
        ...state,
        count: state.count + 1
      }
      case "decrement":
      return {
        ...state,
        count: state.count - 1
      }
      case "colorChange":
      return {
        ...state,
        color: "#"+action.payload
      }
    default:
      break;
  }
  return state
}

//initial state
const initialState = {
  count: 0,
  color: ""
}

export default function App() {
  const [state, dispatch] = useReducer(counterReducer, initialState)

  function coloring() {
    let randomColor = Math.floor(Math.random()*16777215).toString(16);
    dispatch({type: "colorChange", payload: randomColor})
  }

  return (
      <div className="App">
        <p style={{color: state.color}}>Counter: {state.count}</p>
        <button onClick={()=> dispatch({type: "decrement"})}>-</button>
        <button onClick={()=> dispatch({type: "increment"})}>+</button>
        <button onClick={coloring}>color</button>
        </div>
  );
}

More examples on useReducer

We now understand how to use the useReducer hook. But if you are anything like me, it takes a little more practice before anything sticks, Let's check out a more complex example on how to use the useReducer hook.

Creating a Todo App using the useReducer.

Here is a step by step process we take to create Our Todo App

Firstly we initialize the useReducer hook in our component. Using todoReducer as our reducer function and initialTodo as our initial state.

  const [todos, dispatch] = useReducer(todoReducer, initialTodo);

Next, define the reducer function we used in the useReducer argument which is todoReducer.

In the function below we are using a switch case statement to perform different actions based on different conditions. The switch case specifies the action type and the code block to be executed.

//Reducer Function
const todoReducer = (state, action) => {
  switch (action.type) {
    case "ADD_TODO": {
      return [...state, newTodo(action.payload.name)];
    }
    case "DO_TODO":
      return state.map((todo) => {
        if (todo.id === action.id) {
          return { ...todo, complete: true };
        } else {
          return todo;
        }
      });
    case "UNDO_TODO":
      return state.map((todo) => {
        if (todo.id === action.id) {
          return { ...todo, complete: false };
        } else {
          return todo;
        }
      });
    default:
      return state;
  }
};

function newTodo(name) {
  return { id: Date.now(), task: name, complete: false };
}

then create an initialState which is an array object initialTodo.

In the initial state, we are creating an array object containing our first todo list item. Which are the task to be carried out, Using Date.now() for the id to give our todo items a unique id on creation, and a complete property.

//InitialState
const initialTodo = [{ id: Date.now(), task: "Do Something", complete: false }];

In our component where we want to update the state or carry out an action, we call the dispatch function stating the action type we want to carry out.

Where the property type is the action type to be carried out, and the payload property is the value we are passing to the Reducer function.

//Dispatch function
 dispatch({ type: "ADD_TODO", payload: { name: name } });

Putting all of the code together our code looks like this:

import React, { useState, useReducer } from "react";


//InitialState
const initialTodo = [{ id: Date.now(), task: "Do Something", complete: false }];

//Reducer Function
const todoReducer = (state, action) => {
  switch (action.type) {
    case "ADD_TODO": {
      return [...state, newTodo(action.payload.name)];
    }
    case "DO_TODO":
      return state.map((todo) => {
        if (todo.id === action.id) {
          return { ...todo, complete: true };
        } else {
          return todo;
        }
      });
    case "UNDO_TODO":
      return state.map((todo) => {
        if (todo.id === action.id) {
          return { ...todo, complete: false };
        } else {
          return todo;
        }
      });
    default:
      return state;
  }
};

function newTodo(name) {
  return { id: Date.now(), task: name, complete: false };
}

const App = () => {
  const [todos, dispatch] = useReducer(todoReducer, initialTodo);
  const [name, setName] = useState("");

  //CheckBox
  const handleChange = (todo) => {
//Dispatch function
    dispatch({ type: todo.complete ? "UNDO_TODO" : "DO_TODO", id: todo.id });
  };

  //SUbmit New Todo
  const handleSubmit = (e) => {
    e.preventDefault();
//Dispatch function
    dispatch({ type: "ADD_TODO", payload: { name: name } });
    setName("");
  };

  return (
    <div>
      <form onSubmit={handleSubmit}>
        <input
          type="text"
          value={name}
          onChange={(e) => setName(e.target.value)}
        />
      </form>

      <ul>
        {todos.map((todo) => (
          <li key={todo.id}>
            <label>
              <input
                type="checkbox"
                checked={todo.complete}
                onChange={() => handleChange(todo)}
              />
              {todo.task}
            </label>
          </li>
        ))}
      </ul>
    </div>
  );
};
export default App;

When to use the useReducer Hook:

When your application architecture is a large codebase, complex and big in size

If logic to state is super complex or you want to update the state deep down in your component tree

When the initial state value is either an object or an array

useReducer Hook is preferable to handle complex state logic in our app, it is also important to note, that there are some cases where a third party state management library like Redux or MobX would be preferable to use. Some of these cases are:

And, When not to use the useReducer Hook

Your application is small and not so complex

The state value is a primitive value, the state to be stored is not an object or array

Easy state transitions in the component. where logic is not complex and large

Conclusion

The useReducer hook comes in handy in our react application it allows for a simpler, easy to understand, and well-organized way to manage components' states and makes the distribution of data easy between components.

Are you currently using the useReducer hook? What’s your experience with using useReducer?