React Custom Hooks vs. Helper Functions – When to Use Which

This article is a translation of the original article «React Custom Hooks vs. Helper Functions – When To Use Both“.

I also run a telegram channel “Frontend in naval style“, where I talk about interesting things from the world of interface development.

Introduction

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

Today we'll look at the similarities between these two concepts and draw conclusions about when it's best to use each one.

First, let's look at what React Custom Hooks are.

What are React Custom Hooks?

React Custom Hooks are JavaScript functions that give you the ability to reuse stateful logic throughout your React codebase. When using Custom Hooks, we leverage the React Hooks API, which allows us to manage state and its side effects in a way that is consistent with the React functional component process.

One of the unique features of Custom Hooks is the ability to leverage state management, which means we can access React's built-in hooks such as useState And useEffectAnother unique identifier is the fact that we have to follow the naming conventions for React Custom Hooks, as we have to prefix the beginning of the hook with the word use followed by its name, e.g. useFetch.

When we use custom hooks, they can access and modify the component's state and lifecycle methods because they are deeply coupled with the logic behind how React components work.

An example of what a React Custom Hook looks like can be seen 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 contains reusable logic for retrieving data from an API. It can manage loading and error states and can be imported into multiple components.

Now that we have a fundamental understanding of React Custom Hooks, let's look at how they differ from Helper Functions.

What are Helper Functions?

Helper Functions are essentially standalone functions that are used to perform various calculations or tasks. These functions can be used anywhere in the application because they are not part of a React component or state management system. Another key difference is that they can be used in many 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 related to the given input data. They cannot interact with side effects or component states. They also do not require predefined naming conventions such as usage and should be named according to the task you assign them to.

Take a look at this helper function in the 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 Day.js Javascript date library to parse and format the date, giving us a more powerful method of formatting our dates.

Now that we have a refreshed understanding of React Custom Hooks and Helper Functions, it's time to look at how we can use them in a simple application. In the next section, we'll create an application that uses both.

Creating an application using a custom hook and helper function

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

We get data and information about Pokemon from Pokémon APIand then use that data to create our app, which is then styled using Tailwind CSS. Now that we've finished the explanation, it's time to start creating our app.

You can find the code here at GitHub.

The first thing we need to do is create a project structure, so find a place on your computer for the project, such as your desktop, and then run these commands to create a Next.js project.

На экране настройки Next.js убедитесь, что вы выбрали "да" для Tailwind CSS и App Router, чтобы наши проекты имели одинаковые настройки. В этом проекте мы будем использовать JavaScript, остальные настройки по умолчанию должны быть в порядке.

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

Now we should have a Next.js project and we should be inside the folder pokemon-pokedex-appso the next step is to install the JavaScript packages that we need for this application. We need to install dayjs to calculate time and dates and uuid to create unique identifiers for our Pokémon.

Install both packages using this command:

npm i dayjs uuid

Now that our packages are installed, we need to create all the files and folders for our project.

Run the command below to create the architecture of our project:

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 folder components for components Header, Pokemon And PokemonDetails.

  • Create a folder hooks for our hook usePokemonwhich gets data from the Pokemon API

  • Create a folder utils for our functions fetch And date.

So in the next steps we will add some code to our files and then our project will be complete, so open the project in your code editor.

The first one will be our file next.config.mjs in our root folder.

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

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

export default nextConfig;

All we're doing in this file is adding an image template for GitHub so we can access Pokemon images in our app without getting errors. We need to define this route for it to be approved.

Now let's get to our file. layout.jsreplacing all the code with the following:

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 the use of the Yanone_Kaffeesatz Google font for our application, which replaces the default Inter font.

File globals.css next on our list, we just need to do some code cleanup.

As 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 app.

That's it for the initial configuration files, all that's left to do is add the code for our components, hooks, utility, and main page, and our application will be ready.

Starting from the top, let's make our component Header.js inside 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 displays the title of our application – Pokémon, and also shows the date and time. This is achieved by importing a helper function utils/dateUtils.jswhich uses the JavaScript library dayjs to calculate time and date.

The next file to work with will be the file Pokemon.js 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 is our main Pokémon component file that uses our hook usePokemon.js to get data from the Pokémon API. It works in conjunction with our utility file fetchUtils.js to receive the data. We have some error handling set up when receiving the data, and our state data is passed to the PokemonDetails.js component that renders our UI.

So we need to add some code to our file PokemonDetails.js to folder PokemonDetails.

Place 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>
      ))}
    </>
  );
}

Almost all of the code in this file is used to create the interface of our Pokémon app. Styling is done with Tailwind CSS.

There are still a few files left to make before the project is complete. The next file to work on is the file usePokemon.js 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 to get data from an API, and in our case it will be the Pokémon API.

Now we will complete our file. dateUtils.js 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 file we use the JavaScript library dayjs to calculate dates and times in any file it is imported into.

So now for our second utility file, fetchUtils.jsadd this code to it:

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 hook usePokemon.js to get data from the API.

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

Here 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 file page.js – this is the main entry point for all our components, and with this code our project is complete.

Run the project with the normal Next.js startup script as shown here, and you'll see the Pokémon Pokédex app in your browser:

npm run dev

Conclusion

Today we learned how important it is to know the differences between helpers and React Custom if you want to develop organized, clean, and manageable code. When reusing stateful logic in React, it is recommended to use custom hooks, as helpers are best suited for stateless, general-purpose work. You can improve the modularity and reusability of your codebase by wisely deciding when to use both.

Similar Posts

Leave a Reply

Your email address will not be published. Required fields are marked *