Эта статья — перевод оригинальной статьи «React Custom Hooks vs. Helper Functions — When To Use Both».

Также я веду телеграм канал «Frontend по‑флотски», где рассказываю про интересные вещи из мира разработки интерфейсов.

Вступление

При работе довольно часто приходится сталкиваться с различными технологиями и сценариями использования на ежедневной основе. Две популярные концепции - это React Custom Hooks и Helper functions. Концепция Helper functions существует уже очень давно, в то время как React Custom Hooks все еще достаточно современна. Обе концепции позволяют разработчикам абстрагироваться и повторно использовать код, который они пишут, разными способами, хотя они оба имеют немного разные сценарии использования.

Сегодня мы рассмотрим сходства между этими двумя концепциями и сделаем вывод о том, когда правильнее всего использовать каждую из них.

Для начала давайте рассмотрим, что такое React Custom Hooks.

Что такое React Custom Hooks?

React Custom Hooks — это JavaScript‑функции, которые дают вам возможность повторно использовать логику с состоянием во всей кодовой базе React. При использовании Custom Hooks мы используем API React Hooks, который позволяет нам управлять состоянием и его побочными эффектами в соответствии с процессом функциональных компонентов React.

Одной из уникальных характеристик Custom Hooks является возможность использовать управление состоянием, что означает, что мы можем получить доступ к встроенным в React хукам, таким как useState и useEffect. Другим уникальным идентификатором является тот факт, что мы должны следовать именованным соглашениям для React Custom Hooks, поскольку мы должны префиксировать начало хука словом use, за которым следует е��о имя, например, useFetch.

Когда мы используем пользовательские хуки, они могут получать доступ и изменять состояние компонента, а также методы жизненного цикла, поскольку они глубоко связаны с логикой работы компонентов React.

Пример того, как выглядит React Custom Hook, мы можем увидеть в этом примере кода:

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 };
}

Этот пользовательский хук называется useFetch и содержит логику многократного использования для получения данных из API. Он может управлять состояниями загрузки и ошибок и может быть импортирован в несколько компонентов.

Теперь, когда у нас есть фундаментальное понимание React Custom Hooks, давайте посмотрим, чем они отличаются от Helper Functions.

Что такое Helper Functions?

Helper Functions — это, по сути, самостоятельные функции, которые используются для выполнения различных вычислений или задач. Такие функции можно использовать в любом месте приложения, поскольку они не являются частью компонента React или системы управления состоянием. Еще одно ключевое отличие заключается в том, что они могут использоваться во многих языках программирования и не привязаны к какой‑либо экосистеме. Они представляют собой концепцию, которую можно использовать где угодно.

В отличие от пользовательских функций React, функции‑помощники выполняют вычисления и задачи, относящиеся к заданным входным данным. Они не могут взаимодействовать с побочными эффектами или состояниями компонентов. Они также не нуждаются в предопределенных соглашениях об именовании, таких как использование, и должны быть названы в соответствии с задачей, для которой вы их назначили.

Посмотрите на эту вспомогательную функцию в примере здесь:

import dayjs from 'dayjs';

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

В этом фрагменте кода мы используем Javascript-библиотеку дат Day.js для разбора и форматирования даты, что дает нам более мощный метод форматирования наших дат.

Теперь, с нашим обновленным пониманием React Custom Hooks и Helper Functions, самое время посмотреть, как мы можем использовать их в простом приложении. В следующем разделе мы создадим приложение, которое будет использовать оба варианта.

Создание приложения, использующего пользовательский крючок и вспомогательную функцию

Приложение, которое мы будем создавать, — это простое приложение Pokémon Pokédex, которое вы можете видеть на этом рисунке.

Мы получаем данные и информацию о покемонах из Pokémon API, а затем используем эти данные для создания нашего приложения, которое затем оформляется с помощью Tailwind CSS. После того как мы закончили объяснение, пора приступать к созданию нашего приложения.

Вы можете найти код здесь, на GitHub.

Первое, что нам нужно сделать, — это создать структуру проекта, поэтому найдите на компьютере место для проекта, например рабочий стол, а затем выполните эти команды, чтобы создать проект Next.js.

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

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

Теперь у нас должен быть проект Next.js, и мы должны находиться внутри папки pokemon-pokedex-app, поэтому следующим шагом будет установка пакетов JavaScript, которые нам нужны для этого приложения. Нам нужно установить dayjs для вычисления времени и дат и uuid для создания уникальных идентификаторов для наших покемонов.

Установите оба пакета с помощью этой команды:

npm i dayjs uuid

Теперь, когда наши пакеты установлены, нам нужно создать все файлы и папки для нашего проекта.

Выполните приведенную ниже команду, чтобы создать архитектуру нашего проекта:

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 ../..

С помощью этой команды мы:

  • Создаём папку components для компонентов Header, Pokemon и PokemonDetails.

  • Создаём папку hooks для нашего хука usePokemon, который получает данные из Pokemon API

  • Создаём папку utils для наших функций fetch и date.

Итак, в следующих шагах мы добавим код в наши файлы, после чего наш проект будет завершен, поэтому откройте проект в редакторе кода.

Первым будет наш файл next.config.mjs в нашей корневой папке.

Замените весь код в этом файле на новый код здесь:

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

export default nextConfig;

Все, что мы делаем в этом файле, — это добавляем шаблон изображения для GitHub, чтобы мы могли обращаться к изображениям покемонов в нашем приложении, не получая ошибок. Мы должны определить этот маршрут, чтобы он был одобрен.

Теперь давайте займемся нашим файлом layout.js, заменив весь код на приведенный ниже:

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

Основное изменение в этом файле — использование шрифта Yanone_Kaffeesatz Google для нашего приложения, который заменяет стандартный шрифт Inter.

Файл globals.css следующий в нашем списке, нам просто нужно сделать некоторую очистку кода.

Как и раньше, замените код на этот фрагмент:

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

body {
  font-size: 20px;
}

Мы очистили часть кода и сделали размер шрифта по умолчанию 20px для нашего приложения.

На этом с файлами начальной конфигурации покончено, осталось добавить код для наших компонентов, хуков, утилиты и главной страницы, и наше приложение будет готово.

Начиная с самого верха, давайте сделаем наш компонент Header.js внутри Header/Header.js.

Добавьте этот код в наш файл:

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

Этот компонент отображает заголовок нашего приложения — Pokémon, а также показывает дату и время. Это достигается импортом вспомогательной функции utils/dateUtils.js, которая использует JavaScript-библиотеку dayjs для вычисления времени и даты.

Следующим файлом для работы будет файл Pokemon.js в папке Pokemon.

Вот код для нашего файла:

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

Это наш основной файл компонента Pokémon, который использует наш хук usePokemon.js для получения данных из Pokémon API. Он работает вместе с нашим утилитарным файлом fetchUtils.js для получения данных. У нас есть настройка обработки ошибок при получении данных, а наши данные о состоянии передаются в компонент PokemonDetails.js, который отображает наш пользовательский интерфейс.

Итак, мы должны добавить код для нашего файла PokemonDetails.js в папку PokemonDetails.

Поместите этот код в файл:

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

Практически весь код в этом файле используется для создания интерфейса нашего приложения Pokémon. Стилизация выполнена с помощью Tailwind CSS.

Осталось сделать еще несколько файлов до завершения проекта. Следующим файлом для работы будет файл usePokemon.js в нашей папке hooks.

Нашему файлу понадобится этот код, поэтому добавьте его сейчас:

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;

Этот пользовательский хук используется для получения данных из API, и в нашем случае это будет API Pokémon.

Теперь мы завершим наш файл dateUtils.js в папке utils этим кодом:

import dayjs from 'dayjs';

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

С помощью этого файла мы используем JavaScript-библиотеку dayjs для вычисления дат и времени в любом файле, в который она импортируется.

Итак, теперь для нашего второго файла утилит, fetchUtils.js, добавьте в него этот код:

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;
  }
};

Этот файл утилиты работает с нашим хуком usePokemon.js для получения данных из API.

Наконец, давайте завершим наш проект, заменив и добавив код в наш файл main page.js в корневой папке.

Вот код, который нам нужен для этого файла:

'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>
  );
}

Наш файл page.js — это главная точка входа для всех наших компонентов, и с этим кодом наш проект завершен.

Запустите проект с помощью обычного сценария запуска Next.js, как показано здесь, и вы увидите в браузере приложение Pokémon Pokédex:

npm run dev

Заключение

Сегодня мы узнали, как важно знать различия между вспомогательными функциями и React Custom, если вы хотите разрабатывать организованный, чистый и управляемый код. При повторном использовании логики с состоянием в React рекомендуется использовать пользовательские хуки, поскольку вспомогательные функции лучше всего подходят для работы без состояния, общего назначения. Можно повысить модульность и удобство повторного использования вашей кодовой базы, если правильно решить, когда использовать оба варианта.