Building Counter App with useReducer and Custom Hook (Altschool Second-Semester Exam)

Building Counter App with useReducer and Custom Hook (Altschool Second-Semester Exam)

In the AltSchool second semester examination held the last year 2022, we were given the option to pick one question out of four to work on using React Js. The second question caught my attention and I decided to go with it, below is the question:

Setup a custom counter hook with increment, decrement, reset, setValue function with a valid UI and implement a combination of states with a useReducer that implements a counter with the four evident features in the custom hook - increment, decrement, reset, setValue. Implement a page for the custom hook, useReducer, 404, and a page to test error boundary and good SEO.

At first, I was confused about the question but after deliberating on it with my circle member I was able to come up with a solution and I will take you through how I was able to build a counter app.

Setting Up The Project

We all know that every React app starts with a line of code in Git Bash or any other command center:

npx create-react-app examproject-app

Next, I open the project in your code editor which is Virtual Studio Code and created a new folder called Components where I created the following files: Counter.js, ErrorBoundary.js, ErrorPage.js, NavBar.js, and useCount.js. Each component also has a CSS file for styling.

Defining the useCount hook

In the useCount.js file, I define a function called useCount that returns an object containing the current count and the four functions that modify the count.

The useCount hook takes in two arguments: an initial count and an optional step value. The initial count is the value that the counter will be reset to when the reset function is called, and the step value is the amount by which the count will be incremented or decremented when the increment or decrement functions are called, respectively.

Here is the skeleton of the useCount hook:

import { useReducer } from 'react';

const useCount = () => {
  // Define the reducer function
  const countReduce = (state, action) => {
    // TODO: Implement the increment, decrement, reset, and setValue actions
  };

  // Use the useReducer hook to manage the state of the counter
  const [count, dispatch] = useReducer(countReduce, initialCount);

  // TODO: Define the increment, decrement, reset, and setValue functions

  return { state, setValue, handleAddition, handleSubstraction, handleReset };
};

export default useCount;

Implementing the increment, decrement, reset, and setValue actions

To implement the increment, decrement, reset, and setValue actions, l dispatch an action object with a type property. The type of property will be either 'INCREMENT' or 'DECREMENT', reset or setValue depending on the action being performed. Each property will return a count value depending on the action performed on it at that state.

Here is the updated reducer function that implements the increment, decrement, reset, and setValue actions:

const countReduce = (state, action) => {
    if (action.type === "ADD") {
      return { count: state.count + 1 };
    }
    if (action.type === "SUBSTRACT") {
      return { count: state.count - 1 };
    }
    if (action.type === "RESET") {
      return { count: 0 };
    }
    if (action.type === "VALUE") {
      return { count: action.value * 1 };
    }
  };

Defining the four function

Now that the reducer function has been implemented, I define the four functions that modify the count: increment, decrement, reset, and setValue, and also return them.

To define these functions, I use the dispatch function provided by the useReducer hook to dispatch the appropriate action.

Here is the final version of the useCount hook, including the four functions:

import { useReducer } from "react";

const initialState = {
  count: 0,
};

const useCount = () => {
  const countReduce = (state, action) => {
    if (action.type === "ADD") {
      return { count: state.count + 1 };
    }
    if (action.type === "SUBSTRACT") {
      return { count: state.count - 1 };
    }
    if (action.type === "RESET") {
      return { count: 0 };
    }
    if (action.type === "VALUE") {
      return { count: action.value * 1 };
    }
  };
  const [state, dispatch] = useReducer(countReduce, initialState);

  const handleAddition = () => {
    dispatch({
      type: "ADD",
    });
  };
  const handleSubstraction = () => {
    dispatch({
      type: "SUBSTRACT",
    });
  };
  const handleReset = () => {
    dispatch({
      type: "RESET",
    });
  };
  const setValue = (e) => {
    dispatch({
      type: "VALUE",
      value: e.target.value,
      key: "number",
    });
  };
  return {
    state,
    setValue,
    handleAddition,
    handleSubstraction,
    handleReset,
  };
};

export default useCount;

Using the useCount Hook in the React Component

To use the useCount hook in my React component, I import the hook and use it to manage the state of the counter. The hook returns an object containing the current count and the four functions that modify the count.

Here is the useCount hook in a React component:

import React from "react";
import "./Counter.css";
import useCount from "./useCount";

const Counter = () => {
  const { state, handleReset, handleSubstraction, handleAddition, setValue } =
    useCount();
  return (
    <>
      <div className="container">
        <div className="count">
          <h2>{state.count}</h2>
        </div>
        <div className="btn">
          <button className="btn1" onClick={handleAddition}>
            Increment
          </button>
          <button className="btn2" onClick={handleReset}>
            Reset
          </button>
          <button className="btn3" onClick={handleSubstraction}>
            Decrement
          </button>
        </div>
        <div>
          <label>
            <button className="btn4">Set Value</button>
          </label>
          <input type="number" value={state.number} onChange={setValue} />
        </div>
      </div>
    </>
  );
};

export default Counter;

This component will render a count and four buttons which when clicked, increase the count, decrease the count, reset the count to its initial value or set the count to a specified value.

Creating NavBar for the project

I decided to create NavBar.js under my component to use for my navigation on the app (the main reason was to refresh my memory). I start by installing react-router-dom using the code below in my terminal.

npm install react-router-dom

After installing the react-router-dom, I Link in the NavBar.js. The code is below

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

After importing Link, I create a NavBar function where I return the Counter enclosed in Link.

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

const NavBar = () => {
  return (
    <div className="navl">
      <nav className="hom">
        <Link to="/">Counter</Link>
      </nav>
    </div>
  );
};

export default NavBar;

On my App.js, I import BrowserRouter, Routes and Route from "react-router-dom"

import { BrowserRouter, Routes, Route } from "react-router-dom"

Also, I import NavBar.js and the Counter.js to my App.js

import Counter from "./Components/Counter";
import NavBar from "./Components/NavBar";

In the App.js function, I return the NavBar and Counter using Route to create a navigation bar with all enclosed inside BrowserRouter It is as seen in the code below:


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

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

export default App;

Implementation of the ErrorBoundary Page

To implement a page for testing error boundaries, I created a component that throws an error in its render method. I then wrap this component in an error boundary component to handle the error.

Here is the code:

import React from "react";
import { Component } from "react";

class ErrorBoundary extends Component {
  constructor(props) {
    super(props);
    this.state = { error: null, errorInfo: null };
  }

  componentDidCatch(error, errorInfo) {
    this.setState({
      error: error,
      errorInfo: errorInfo,
    });
  }

  render() {
    if (this.state.errorInfo) {
      return (
        <div>
          <h2>An Error Has Occurred</h2>
          <details>
            {this.state.error && this.state.error.toString()}
            <br />
            {this.state.errorInfo.componentStack}
          </details>
        </div>
      );
    }

    return this.props.children;
  }
}

class Counter extends React.Component {
  constructor(props) {
    super(props);
    this.state = { counter: 0 };
    this.handleClick = this.handleClick.bind(this);
  }

  handleClick() {
    this.setState(({ counter }) => ({
      counter: counter + 1,
    }));
  }

  render() {
    if (this.state.counter === 3) {
      // Simulate a JS error
      throw new Error("Crashed!!!!");
    }
    return <h1 onClick={this.handleClick}>{this.state.counter}</h1>;
  }
}
export default ErrorBoundary;

I import the ErrorBoundary in the App.js component and wrap the NavBar and the Router with it to throw an error when there is one.

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

function App() {
  return (
    <ErrorBoundary>
      <BrowserRouter>
        <NavBar />
        <Routes>
          <Route index element={<Counter />} />
          <Route path="/" element={<Counter />} />
        </Routes>
      </BrowserRouter>
    </ErrorBoundary>
  );
}

export default App;

Implementing an Error page

To implement an error page, I created a component that displays a message and a link to the home page when the user navigates to a non-existent route.

Below is the Error Page code:

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

const ErrorPage = () => {
  return (

      <div className="err">
        <h3>Error 404: Something went wrong...</h3>
        <p>
          Go back to{" "}
          <span>
            <Link to="/">Home</Link>
          </span>
        </p>
      </div>

  );
};

export default ErrorPage;

In the App.js component, import the ErrorPage and include it as part of the Routes. Check the code below

import Counter from "./Components/Counter";
import { BrowserRouter, Routes, Route } from "react-router-dom";
import NavBar from "./Components/NavBar";
import ErrorBoundary from "./Components/ErrorBoundary";
import ErrorPage from "./Components/ErrorPage";

function App() {
  return (
    <ErrorBoundary>
      <BrowserRouter>
        <NavBar />
        <Routes>
          <Route index element={<Counter />} />
          <Route path="/" element={<Counter />} />
          <Route path="*" element={<ErrorPage />} />
        </Routes>
      </BrowserRouter>
    </ErrorBoundary>
  );
}

export default App;

In this example, the FoundPage component will be rendered when the user navigates to a route that does not match any of the routes defined in the component.

Implementing good SEO

To implement good SEO in my Counter app, I ensure the following:

  • Use descriptive, keyword-rich titles and descriptions for your pages.

  • I use clean, semantic HTML markup to structure your content.

  • Use descriptive, keyword-rich URLs for my pages.

  • Use the link and meta tags in the head of your HTML documents to provide additional information about my page using Helmet.

I install Helmet in my app using the command line below

npm install -save react-helmet

I then import Helmet in all my component so that I can use title tags and meta tags. The code is below:

import { Helmet } from "react-helmet-async";

After importing Helmet in all my components, I then use it to create title tag and meta tag. Here is it in my Counter.js component.

import React from "react";
import "./Counter.css";
import useCount from "./useCount";
import { Helmet } from "react-helmet-async";

const Counter = () => {
  const { state, handleReset, handleSubstraction, handleAddition, setValue } =
    useCount();
  return (
    <>
      <Helmet>
        <title>Counter App</title>
        <meta
          name="description"
          content="A counter app using a custom hook with substraction, addition and reset button"
        />
        <link rel="cononical" href="/CounterTwo" />
      </Helmet>
      <div className="container">
        <div className="count">
          <h2>{state.count}</h2>
        </div>
        <div className="btn">
          <button className="btn1" onClick={handleAddition}>
            Increment
          </button>
          <button className="btn2" onClick={handleReset}>
            Reset
          </button>
          <button className="btn3" onClick={handleSubstraction}>
            Decrement
          </button>
        </div>
        <div>
          <label>
            <button className="btn4">Set Value</button>
          </label>
          <input type="number" value={state.number} onChange={setValue} />
        </div>
      </div>
    </>
  );
};

export default Counter;

Conclusion

The above article is about how I set up a custom counter hook with the useReducer hook, implement a combination of states, and implement a page for the custom hook, useReducer, 404, and a page to test error boundaries and good SEO in the Counter app.