How to build counter app with useReducer hook in react

How to build counter app with useReducer hook in react

Table of contents

No heading

No headings in the article.

Introduction

React hooks gives room for a stateful component and manage the local state in it. Hooks are JavaScript functions that manage the state's behavior and side effects by isolating them from a component.

In this article, we will be looking at useReducer Hook and building a simple counter application with it.

Prerequisites

-A basic knowledge of how to create react application

-Knowledge of hooks in react

What is useReducer Hook?
useReducer hook is one of the additional Hooks that was included with React v16.8. It is an alternative to the useState Hook, useReducer helps you manage complex state logic in React applications.

useReducer accepts two (2) parameters, the reducer function and the initial state(s). It returns an array that holds the current state value and a dispatch function that can always be invoked with an action.

We will be building a counter app to understand how it works.

Here are the steps we will follow:

  1. Build React App

  2. Create a counter component file inside the component folder

  3. Create the reducer function

  4. Install react-router

  5. Create an Error boundary component

  6. Create an Error page component

  7. Implement functions in counter component

  8. Summary

Build and run React app
Type the following in your terminal and run the command, to create a react application.

npx create-react-app counter-app

Create a Counter Component file inside the component folder

We can name this Counter.js, here is the code to start with.

const Counter = () => {
return(
    <div className="counter-container">
        <h1>Counter</h1>
    <div>
    )
}
export default Counter;

Import the useReducer Hook from react in the component. We should note that useReducer hook returns a state and dispatch method. The dispatch method update our state based on the action.type in the reducer function, which we will see later as we go on.

import { useReducer } from "react";

const Counter = () => {
const [state, dispatch] = useReducer(reducer, { count: 0, userInput: "" });
return(
    <div className="counter-container">
        <h1>Counter</h1>
    <div>
    )
}
export default Counter;

Create the reducer function inside a hook folder

The reducer function takes in two arguments, the current state and an action and returns based on both arguments a new state.

export const reducer = (state, action) => {
// some code should run
}

We can move ahead with a switch statement or an if-else statement depending on what we want to achieve.

In this reducer function, we have four (4) cases which is also the type of action, "increment" which increases the state by 1, "decrement" which decreases the state by 1 and there is a condition that makes it not pass 0 as we don't want a negative number, "clear" which returns the state to 0 and "set" case which allows the user to input the desired number.

There is always a default type of action or case which will come into action whenever other cases are not in action. Here in our counter app, the default is throwing new Error which we will use an error boundary component to wrap our main component to implement this well, we will be looking at this part later as we go on.

export const reducer = (state, action) => {
  switch (action.type) {
    case "increment":
      return { ...state, count: state.count + 1 };
    case "decrement":
      if (state.count > 0) {
        return { ...state, count: state.count - 1 };
      } else {
        return state;
      }
    case "clear":
      return { ...state, count: (state.count = 0) };
    case "set":
      return {
        ...state,
        count: (state.count = action.payload),
        userInput: action.payload,
      };
    default:
      throw new Error();
  }
};

Further to explain the code above, an action is an object that contains the payload of information. They are the only source of information for the reducer function to be updated. Reducers update their store based on the value of the action.type. Here our set action has a payload.

Another important thing to note in the code above is the spread operator. It helps us to maintain and keep track of the value of the state, this way we can return a new object that is filled with the state that is passed to it and the payload that’s sent by the user.

Install react-router

Type npm i react-router@v6 in the terminal to install react-router. Then the code structure for our App.js component will change. We will import BrowserRouter, Routes, Route from "react-router-dom" and implement the structure below.

import Counter from "./components/Counter";
import { BrowserRouter, Routes, Route } from "react-router-dom";

function App() {
  return (
    <main className="App">
      <BrowserRouter>
        <Routes>
          <Route index element={<Counter />} />
        </Routes>
      </BrowserRouter>
    </main>
  );
}

export default App;

Create an Error page component

Error boundaries are React components that catch JavaScript errors anywhere in their child component tree, log those errors, and display a fallback UI. For more information about error boundaries in react, please visit React Page.

Import Component from "react" and Link from "react-router-dom". We can name our component for this code ErrorBoundary.js.

import React, { Component } from "react";
import { Link } from "react-router-dom";

export class ErrorBoundary extends Component {
  constructor(props) {
    super(props);

    this.state = {
      error: null,
    };
  }
  componentDidCatch(error, errorInfo) {
    console.log({ error, errorInfo });
  }

  static getDerivedStateFromErorr(error) {
    return { error };
  }

  render() {
    if (this.state.error)
      return (
        <p style={{ color: "red" }}>
          Something went wrong!!!
          <Link to="/">Go Back</Link>
        </p>
      );

    return this.props.children;
  }
}

export default ErrorBoundary;

Let's create a fallback UI for our counter app so that our app display this component anytime there is a trigger on the ErrorBoundary component.

import { Link } from "react-router-dom";

const PageNotFound = () => {
  return (
    <h1>
      Something went wrong
      <Link to="/">Go Back</Link>
    </h1>
  );
};

export default PageNotFound;

The component above will be added as another Route so we have this below;

import Counter from "./components/Counter";
import PageNotFound from "./components/PageNotFound";
import { BrowserRouter, Routes, Route } from "react-router-dom";

function App() {
 return (
   <main className="App">
     <BrowserRouter>
       <Routes>
         <Route index element={<Counter />} />
         <Route path="*" element={<PageNotFound />} />
       </Routes>
     </BrowserRouter>
   </main>
 );
}

export default App;

We can import ErrorBoundary component to where we want to use it in our application. Here, it's being imported to index.js so that we can configure it for the entire application. You need to wrap the entire App component with this ErrorBoundary component below.

import React from "react";
import ReactDOM from "react-dom/client";
import App from "./App";
import ErrorBoundary from "./components/ErrorBoundary";

const root = ReactDOM.createRoot(document.getElementById("root"));
root.render(
  <ErrorBoundary>
    <App />
  </ErrorBoundary>
);

Implementing functions in counter component

In this section, we will be implementing all our functions in the Counter.js component. The useReducer hook is imported from "react" and it takes in the reducer function and an object which contains the initial state of count and userInput. The dispatch method is used inside each of our functions which will update the state. The state is being displayed in the <h2>{state.count}</h2> tag.

import { useReducer, useState } from "react";
import { reducer } from "../hook/Reducer";


const Counter = () => {
  const [state, dispatch] = useReducer(reducer, { count: 0, userInput: "" });
  const [openInputModal, setOpenInputModal] = useState(false);

  const handleIncrease = () => {
    dispatch({ type: "increment" });
  };

  const handleDecrease = () => {
    dispatch({ type: "decrement" });
  };

  const handleClear = (e) => {
    dispatch({ type: "clear", payload: "" });
  };

  const handleSetValue = (e) => {
    setOpenInputModal(true);
    let maxLen = String(e.target.value);
    if (maxLen.length < 5) {
      dispatch({ type: "set", payload: maxLen }); 
      // max input length set to 4 digits
    }
  };

  const handleSubmit = () => {
    setOpenInputModal(false);
  };

  return (
    <div className="counter-container">
      <div className="heading">
        <h1>Counter</h1>
      </div>
      <div className="result">
        <h2>{state.count}</h2>
      </div>
      <div className="user-input-container">
        {openInputModal && (
          <div>
            <input
              placeholder="set value"
              type="number"
              value={state.userInput}
              onChange={handleSetValue}
            />
            <button className="ok-btn" onClick={handleSubmit}>
              ok
            </button>
          </div>
        )}
      </div>
      <div className="buttons-container">
        <button className="bttns" onClick={handleIncrease}>
          +
        </button>
        <button className="bttns" onClick={handleDecrease}>
          -
        </button>
        <button className="bttns" onClick={handleClear}>
          C
        </button>
        <button className="bttns" onClick={handleSetValue}>
          set
        </button>
      </div>
    </div>
  );
};

export default Counter;

Here is the UI of the counter app. For full code of the CSS please visit my GitHub

Summary

useReducer hook takes in a reducer function and initial state as an argument and returns the current state and a dispatch method.

It helps to keep track of multiple pieces of state that rely on complex logic which makes it a better alternative for useState hook.

The reducer function takes in two arguments, the current state and an action and returns a new state based on both arguments. An action is an object that contains the payload of information and the payload is the only source of information the reducer understands to make updates.

Error boundaries in react help to catch JavaScript errors anywhere in their child component tree.