Building Counter App with useReducer and Custom Hook (Altschool Second-Semester Exam)
Table of contents
- Setting Up The Project
- Defining the useCount hook
- Implementing the increment, decrement, reset, and setValue actions
- Defining the four function
- Using the useCount Hook in the React Component
- Creating NavBar for the project
- Implementation of the ErrorBoundary Page
- Implementing an Error page
- Implementing good SEO
- Conclusion
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
andmeta
tags in thehead
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.