Эта статья — перевод оригинальной статьи «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 рекомендуется использовать пользовательские хуки, поскольку вспомогательные функции лучше всего подходят для работы без состояния, общего назначения. Можно повысить модульность и удобство повторного использования вашей кодовой базы, если правильно решить, когда использовать оба варианта.

Комментарии (2)


  1. ganqqwerty
    19.07.2024 08:07
    +1

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


  1. Keeper10
    19.07.2024 08:07

    Первый же пример кода содержит некорректный код. В конце примера не хватает нескольких строк с закрывающими скобками.

    Ах да, это же просто реклама телеграм-канала.