React Custom Hooks vs. Helper Functions – When To Use Both

React Custom Hooks vs. Helper Functions – When To Use Both

When working as a developer it is fairly common to come across various technologies and use cases on a day-to-day basis. Two popular concepts are React Custom Hooks and Helper functions. The concept of Helper functions has been around for a very long time whereas React Custom Hooks are still fairly modern. Both concepts allow developers to abstract and reuse the code that they write in different ways although they both have slightly different use cases.

Today we will take a look at the similarities between the two and conclude on when is the right time to use each of them.

Let’s start by taking a look at what React Custom Hooks are.

What are React Custom Hooks?

React Custom Hooks are JavaScript functions which give you the ability to reuse stateful logic throughout your React codebase. When using Custom Hooks we tap into the React Hooks API that lets us manage our state and its side effects in a way that follows the React functional component process.

One of the unique characteristics of Custom Hooks is being able to leverage state management which means that we can access the React built-in hooks like useState, and useEffect. Another unique identifier is the fact that we have to follow the named conventions for React Custom Hooks as we must prefix the start of the hook with the word use followed by its name, for example, useFetch.

When we use Custom Hooks they can access and change a component state, plus lifecycle methods as they are deeply interconnected with the logic for React components.

We can see an example of what a React Custom Hook looks like in this code example:

import { useState, useEffect } from react;

export function useFetch(url) {
const [data, setData] = useState([]);
const [error, setError] = useState(null);
const [isLoading, setIsLoading] = useState(null);

useEffect(() => {
const fetchData = async () => {
try {
const json = await fetch(url).then((r) => r.json());
setIsLoading(false);
setData(json);
} catch (error) {
setError(error);
setIsLoading(false);
}
};

fetchData();

return { data, error, isLoading };
}

This Custom Hook is called useFetch and has reusable logic for fetching data from an API. It can manage the loading and error states and can be imported into multiple components.

Now that we have a fundamental understanding of React Custom Hooks let’s see how they compare to Helper Functions.

What are Helper Functions?

Helper functions are essentially standalone functions which are used for doing different calculations or tasks. These types of functions can be used anywhere inside your applications as they are not part of the React component or state management system. Another key difference is that they can be used in numerous programming languages and are not tied to any ecosystem. They are a concept that can be used anywhere.

Unlike React Custom Functions, Helper functions perform calculations and tasks which are relevant to the given input. They cannot interact with side effects or component states. They also do not need predefined naming conventions like use and should be named based on whatever task you have designated them for.

Take a look at this helper function in this example here:

import dayjs from dayjs;

function formatDate(date) {
return dayjs(date).format(MM/DD/YYYY);
}

export default formatDate;

In this code snippet, we use the Javascript date library Day.js to parse and format the date, which gives us a more powerful method for formatting our dates.

Ok with our updated understanding of React Custom Hooks and helper functions it’s now a great time to see how we can incorporate both of them into a simple application. In the next section, we are going to build an app that uses them both.

Building an app that uses a Custom Hook and Helper Function

The app we will be building is a simple Pokémon Pokédex application which you can see in this picture.

We get the Pokemon data and information from the Pokémon API and then we use the data to build our application which is then styled using Tailwind CSS. With our explanation done it’s time to start building our app.

You can find the codebase online here on GitHub https://github.com/andrewbaisden/pokemon-pokedex-app

The first thing we have to do is set up our project structure so find a location on your computer for the project like the desktop and then run these commands to create a Next.js project.

On the Next.js setup screen make sure that you select yes for Tailwind CSS and the App Router so that our projects have the same setup. In this project, we will be using JavaScript, the other default settings should be fine.

npx create-next-app pokemon-pokedex-app
cd pokemon-pokedex-app

We should now have a Next.js project and we should be inside of the pokemon-pokedex-app folder so the next step is to install the JavaScript packages which we need for this application. We have to install dayjs for calculating times and dates and uuid for creating unique IDs for our Pokémon.

Install both packages with this command:

npm i dayjs uuid

Now that our packages are installed next we are going to create all of the files and folders for our project.

Run the command below to create our project architecture:

cd src/app
mkdir components hooks utils
mkdir components/Header components/Pokemon components/PokemonDetails
touch components/Header/Header.js components/Pokemon/Pokemon.js components/PokemonDetails/PokemonDetails.js
touch hooks/usePokemon.js
touch utils/dateUtils.js utils/fetchUtils.js
cd ../..

With this command we:

Create a components folder for our Header, Pokemon and PokemonDetails components
Create a hooks folder for our usePokemon hook which fetches data from the Pokemon API
Create a utils folder for our fetch and date utility functions

Ok, in the next steps, we shall add the code to our files and then our project will be completed, so open the project in your code editor.

Up first is our next.config.mjs file in our root folder.

Replace all of the code in that file with this new code here:

/** @type {import(‘next’).NextConfig} */
const nextConfig = {
images: {
remotePatterns: [
{
protocol: https,
hostname: raw.githubusercontent.com,
},
],
},
};

export default nextConfig;

All we are doing in this file is adding an image pattern for GitHub so that we can access the Pokémon images in our application without getting an error. We have to define that route so that it is approved.

Now let’s do our layout.js file so replace all of the code with the code below:

import { Yanone_Kaffeesatz } from next/font/google;
import ./globals.css;

const yanone = Yanone_Kaffeesatz({ subsets: [latin] });

export const metadata = {
title: Create Next App,
description: Generated by create next app,
};

export default function RootLayout({ children }) {
return (
<html lang=en>
<body className={yanone.className}>{children}</body>
</html>
);
}

The main change in this file is using the Yanone_Kaffeesatz Google font for our application which replaces the default Inter font.

The globals.css file is next on our list we just have to do some code cleanup.

Like before replace the code with this snippet:

@tailwind base;
@tailwind components;
@tailwind utilities;

body {
font-size: 20px;
}

We cleaned up some of the code and made the default font size 20px for our application.

That takes care of our initial configuration files we just need to add the code for our components, hooks, utils and main page and our application will be ready.

Starting from the top let’s do our Header.js component inside of Header/Header.js.

Add this code to our file:

import { useState, useEffect } from react;
import { getLiveDateTime } from ../../utils/dateUtils;

export default function Header() {
const [dateTime, setDateTime] = useState(getLiveDateTime());

useEffect(() => {
const interval = setInterval(() => {
setDateTime(getLiveDateTime());
}, 1000);

return () => clearInterval(interval);
}, []);
return (
<>
<header className=flex row justify-between items-center bg-slate-900 text-white p-4 rounded-lg>
<div>
<h1 className=text-5xl uppercase>Pokémon</h1>
</div>
<div>
<p>Date: {dateTime.date}</p>
<p>Time: {dateTime.time}</p>
</div>
</header>
</>
);
}

This component essentially displays the title for our application which is Pokémon and it also shows the live date and time. This is accomplished by importing the utils/dateUtils.js helper function that uses the dayjs JavaScript library for calculating the time and date.

The next file to work on will be the Pokemon.js file in the Pokemon folder.

Here is the code for our file:

import { useState, useEffect } from react;
import usePokemon from ../../hooks/usePokemon;
import { fetchPokemon } from ../../utils/fetchUtils;
import PokemonDetails from ../PokemonDetails/PokemonDetails;

export default function Pokemon() {
const { data, isLoading, error } = usePokemon(
https://pokeapi.co/api/v2/pokemon
);

const [pokemonDetails, setPokemonDetails] = useState([]);

useEffect(() => {
const fetchPokemonDetails = async () => {
if (data && data.results) {
const details = await Promise.all(
data.results.map(async (pokemon) => {
const pokemonData = await fetchPokemon(pokemon.url);
return pokemonData;
})
);
setPokemonDetails(details);
}
};

fetchPokemonDetails();
}, [data]);

if (isLoading) {
return <div>Loading</div>;
}

if (error) {
return <div>Error: {error.message}</div>;
}

return (
<>
<div className=flex row flex-wrap gap-4 justify-evenly>
<PokemonDetails pokemonDetails={pokemonDetails} />
</div>
</>
);
}

This our our main Pokémon component file which uses our usePokemon.js hook for fetching data from the Pokémon API. This works alongside our utility fetchUtils.js file for fetching data. We have an error handling setup for fetching data and our state data is passed down into our PokemonDetails.js component that renders our user interface.

Right, we should add the code for our PokemonDetails.js file now in the PokemonDetails folder.

Put this code in the file:

import Image from next/image;
import { v4 as uuidv4 } from uuid;

export default function PokemonDetails({ pokemonDetails }) {
return (
<>
{pokemonDetails.map((pokemon) => (
<div
key={pokemon.id}
className={
pokemon.types[0].type.name === fire
? bg-orange-400
: pokemon.types[0].type.name === water
? bg-blue-400
: pokemon.types[0].type.name === grass
? bg-green-400
: pokemon.types[0].type.name === bug
? bg-green-700
: pokemon.types[0].type.name === normal
? bg-slate-400
:
}
>
<div className=text-white p-4>
<div className=capitalize>
<h1 className=text-4xl>{pokemon.name}</h1>
</div>
<div className=flex row gap-2 mt-4 mb-4>
<div className=bg-indigo-500 shadow-lg shadow-indigo-500/50 p-2 rounded-lg text-sm>
Height: {pokemon.height}
</div>
<div className=bg-indigo-500 shadow-lg shadow-indigo-500/50 p-2 rounded-lg text-sm>
Weight: {pokemon.weight}
</div>
</div>
<div className=bg-white text-black rounded-lg p-4>
{pokemon.stats.map((stat) => (
<div key={uuidv4()}>
<div className=capitalize flex row items-center gap-2>
<table>
<tr>
<td width={110}>{stat.stat.name}</td>
<td width={40}>{stat.base_stat}</td>
<td width={40}>
<div
style={{
width: `${stat.base_stat}px`,
height: 0.5rem,
backgroundColor: `${
stat.base_stat <= 29
? red
: stat.base_stat <= 60
? yellow
: green
}`,
}}
></div>
</td>
</tr>
</table>
</div>
</div>
))}
</div>
<div>
<Image
priority
alt={pokemon.name}
height={300}
width={300}
src={pokemon.sprites.other.home.front_default}
/>
</div>
</div>
</div>
))}
</>
);
}

Pretty much all of the code in this file is used for creating the interface for our Pokémon application. The styling is done using Tailwind CSS.

Just a few more files to do before the project is done. The next file to work on will be the usePokemon.js file in our hooks folder.

Our file will need this code so add it now:

import { useState, useEffect } from react;
import { fetchPokemon } from ../utils/fetchUtils;

const usePokemon = (initialUrl) => {
const [data, setData] = useState(null);
const [isLoading, setIsLoading] = useState(true);
const [error, setError] = useState(null);

useEffect(() => {
const fetchData = async () => {
try {
const pokemonData = await fetchPokemon(initialUrl);
setData(pokemonData);
} catch (error) {
setError(error);
} finally {
setIsLoading(false);
}
};
fetchData();
}, [initialUrl]);
return { data, isLoading, error };
};

export default usePokemon;

This custom hook is used for fetching data from an API and in our case it will be for the Pokémon API.

Now we will complete our dateUtils.js file in the utils folder with this code:

import dayjs from dayjs;

export const getLiveDateTime = () => {
const now = dayjs();
return {
date: now.format(MMMM D, YYYY),
time: now.format(h:mm:ss A),
};
};

With this utility file, we use the dayjs JavaScript library to calculate dates and times in any file it is imported into.

Ok now for our second utility file, fetchUtils.js add this code to the file:

export const fetchPokemon = async (url) => {
try {
const response = await fetch(url);
const data = await response.json();
console.log(data);
return data;
} catch (error) {
console.error(Error fetching Pokemon:, error);
throw error;
}
};

This utility file works with our usePokemon.js hook to fetch data from an API.

Finally, let’s complete our project by replacing and adding the code to our main page.js file in the root folder.

This is the code we need for this file:

use client;
import Header from ./components/Header/Header;
import Pokemon from ./components/Pokemon/Pokemon;

export default function PokemonList() {
return (
<div className=p-5>
<Header />
<h1 className=text-4xl mt-4 mb-4>Pokédex</h1>
<Pokemon />
</div>
);
}

Our page.js file is the main entry point for all of our components and with this code, our project is now completed.

Run your project using the usual Next.js run script as shown here and you should see the Pokémon Pokédex application in your browser:

npm run dev

Conclusion

Today we learned how it’s important to know the differences between helper functions and React Custom if you want to develop code that is organised, clean, and manageable. With stateful logic reuse in React, custom hooks are recommended because helper functions work best for stateless, general-purpose jobs. It is possible to enhance your codebase’s modularity and reusability by making the best judgement on when to use both.