Хочу рассказать о пяти простых React-хуках, которые пригодятся в любом проекте. Причём, полезность этих хуков не зависит от того, в каком именно приложении их будут использовать. Описывая каждый из них, я рассказываю о его реализации и привожу пример его использования в клиентском коде.



Хук useModalState


Модальные окна широко используются в веб-приложениях, они применяются в самых разных ситуациях. При работе с подобными окнами быстро приходит понимание того, что управление их состоянием — это трудоёмкая задача, решая которую, приходится постоянно выполнять одни и те же рутинные действия. А если имеется код, написание которого отнимает немало сил и времени, код, который приходится постоянно повторять в разных местах приложения, это значит, что такой код имеет смысл абстрагировать, оформив в виде самостоятельной сущности. Хук useModalState — это именно такой код, используемый для управления состоянием модальных окон.

Собственные версии этого хука предоставляют многие библиотеки. Одна из них — это Chakra UI. Если вас интересуют подробности об этой библиотеке — вот мой материал о ней.

Реализация useModalState весьма проста, даже тривиальна. Но опыт подсказывает мне, что гораздо лучше пользоваться им, чем постоянно заново писать код для управления состоянием модальных окон.

Вот код этого хука:

import React from "react";
import Modal from "./Modal";

export const useModalState = ({ initialOpen = false } = {}) => {
  const [isOpen, setIsOpen] = useState(initialOpen);

  const onOpen = () => {
    setIsOpen(true);
  };

  const onClose = () => {
    setIsOpen(false);
  };

  const onToggle = () => {
    setIsOpen(!isOpen);
  };

  return { onOpen, onClose, isOpen, onToggle };
};

А вот — пример его использования:

const Client = () => {
  const { isOpen, onToggle } = useModalState();

  const handleClick = () => {
    onToggle();
  };

  return (
    <div>
      <button onClick={handleClick} />
      <Modal open={isOpen} />
    </div>
  );
};

export default Client;

Хук useConfirmationDialog


Хук useConfirmationDialog тоже имеет отношение к модальным окнам. И им я тоже пользуюсь довольно часто. Если пользователь некоего приложения выполняет какие-то важные действия, вроде удаления чего-либо, у него принято запрашивать подтверждение выполнения подобных действий. Поэтому такую логику имеет смысл абстрагировать в виде хука. Вот — один из вариантов реализации хука useConfirmationDialog:

import React, { useCallback, useState } from 'react';
import ConfirmationDialog from 'components/global/ConfirmationDialog';

export default function useConfirmationDialog({
    headerText,
    bodyText,
    confirmationButtonText,
    onConfirmClick,
}) {
    const [isOpen, setIsOpen] = useState(false);

    const onOpen = () => {
        setIsOpen(true);
    };

    const Dialog = useCallback(
        () => (
            <ConfirmationDialog
                headerText={headerText}
                bodyText={bodyText}
                isOpen={isOpen}
                onConfirmClick={onConfirmClick}
                onCancelClick={() => setIsOpen(false)}
                confirmationButtonText={confirmationButtonText}
            />
        ),
        [isOpen]
    );

    return {
        Dialog,
        onOpen,
    };
}

Вот — пример его использования:

import React from "react";
import { useConfirmationDialog } from './useConfirmationDialog'

function Client() {
  const { Dialog, onOpen } = useConfirmationDialog({
    headerText: "Delete this record?",
    bodyText:
      "Are you sure you want delete this record? This cannot be undone.",
    confirmationButtonText: "Delete",
    onConfirmClick: handleDeleteConfirm,
  });

  function handleDeleteConfirm() {
    //TODO: удалить
  }

  const handleDeleteClick = () => {
    onOpen();
  };

  return (
    <div>
      <Dialog />
      <button onClick={handleDeleteClick} />
    </div>
  );
}

export default Client;

Тут стоит обратить внимание на то, что эта реализация useConfirmationDialog нормально работает до тех пор, пока в модальном окне подтверждения операции нет управляемых элементов, представленных полями для ввода данных. Если в вашем окне такие элементы имеются — лучше создать для такого модального окна отдельный компонент. Дело тут в том, что вам вряд ли понравится то, что содержимое модального окна, включая и такие поля, будет повторно рендериться каждый раз, когда пользователь вводит в поля какие-то данные.

Хук useAsync


Грамотная поддержка асинхронных операций в приложении — это задача, решить которую сложнее, чем кажется на первый взгляд. Так, может иметься множество переменных, хранящихся в состоянии, за которыми нужно наблюдать в процессе выполнения подобных операций. Приложение может сообщать пользователю о ходе выполнения асинхронной операции, показывая ему индикатор прогресса. Кроме того, нужно обрабатывать ошибки асинхронных операций и, если они происходят, выдавать адекватные сообщения о них. В результате наличие в React-проекте хорошо проработанного фреймворка, обеспечивающего поддержку асинхронных операций, окупится сторицей. Хук useAsync может оказаться полезным именно для решения вышеописанных задач. Вот его код:

export const useAsync = ({ asyncFunction }) => {
  const [loading, setLoading] = useState(false);
  const [error, setError] = useState(null);
  const [result, setResult] = useState(null);

  const execute = useCallback(
    async (...params) => {
      try {
        setLoading(true);
        const response = await asyncFunction(...params);
        setResult(response);
      } catch (e) {
        setError(e);
      }
      setLoading(false);
    },
    [asyncFunction]
  );

  return { error, result, loading, execute };
};

А ниже показан пример его использования:

import React from "react";

export default function Client() {
  const { loading, result, error, execute } = useAsync({
    asyncFunction: someAsyncTask,
  });

  async function someAsyncTask() {
    // выполнение асинхронной операции
  }

  const handleClick = () => {
    execute();
  };

  return (
    <div>
      {loading && <p>loading</p>}
      {!loading && result && <p>{result}</p>}
      {!loading && error?.message && <p>{error?.message}</p>}
      <button onClick={handleClick} />
    </div>
  );
}

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

Хук useTrackErrors


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

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

import React, { useState } from "react";
import FormControl from "./FormControl";
import Input from "./Input";
import onSignup from "./SignupAPI";

export const useTrackErrors = () => {
  const [errors, setErrors] = useState({});

  const setErrors = (errsArray) => {
    const newErrors = { ...errors };
    errsArray.forEach(({ key, value }) => {
      newErrors[key] = value;
    });

    setErrors(newErrors);
  };

  const clearErrors = () => {
    setErrors({});
  };

  return { errors, setErrors, clearErrors };
};

Вот как можно пользоваться этим хуком:

import React, { useState } from "react";
import FormControl from "./FormControl";
import Input from "./Input";
import onSignup from "./SignupAPI";

export default function Client() {
  const { errors, setErrors, clearErrors } = useTrackErrors();

  const [name, setName] = useState("");
  const [email, setEmail] = useState("");

  const handleSignupClick = () => {
    let invalid = false;

    const errs = [];
    if (!name) {
      errs.push({ key: "name", value: true });
      invalid = true;
    }
    if (!email) {
      errs.push({ key: "email", value: true });
      invalid = true;
    }
    if (invalid) {
      setErrors(errs);
      return;
    }

    onSignup(name, email);
    clearErrors();
  };

  const handleNameChange = (e) => {
    setName(e.target.value);
    setErrors([{ key: "name", value: false }]);
  };

  const handleEmailChange = (e) => {
    setEmail(e.target.value);
    setErrors([{ key: "email", value: false }]);
  };

  return (
    <div>
      <FormControl isInvalid={errors["name"]}>
        <FormLabel>Full Name</FormLabel>
        <Input
          onKeyDown={handleKeyDown}
          onChange={handleNameChange}
          value={name}
          placeholder="Your name..."
        />
      </FormControl>
      <FormControl isInvalid={errors["email"]}>
        <FormLabel>Email</FormLabel>
        <Input
          onKeyDown={handleKeyDown}
          onChange={handleEmailChange}
          value={email}
          placeholder="Your email..."
        />
      </FormControl>
      <button onClick={handleSignupClick}>Sign Up</button>
    </div>
  );
}

Хук useDebounce


То, что называется «debouncing», способно найти применение в любом приложении. В частности, речь идёт об уменьшении частоты выполнения ресурсоёмких операций. Например, это — предотвращение вызова API поиска данных после каждого нажатия на клавишу в ходе ввода пользователем поискового запроса. Обращение к API будет выполнено после того, как пользователь завершит ввод данных. Хук useDebounce упрощает решение подобных задач. Вот — его простая реализация, которая основана на AwesomeDebounceLibrary:

import AwesomeDebouncePromise from "awesome-debounce-promise";

const debounceAction = (actionFunc, delay) =>
  AwesomeDebouncePromise(actionFunc, delay);

function useDebounce(func, delay) {
  const debouncedFunction = useMemo(() => debounceAction(func, delay), [
    delay,
    func,
  ]);

  return debouncedFunction;
}

Вот — практический пример использования этого хука:

import React from "react";

const callAPI = async (value) => {
  // вызов “дорогого” API
};

export default function Client() {
  const debouncedAPICall = useDebounce(callAPI, 500);

  const handleInputChange = async (e) => {
    debouncedAPICall(e.target.value);
  };

  return (
    <form>
      <input type="text" onChange={handleInputChange} />
    </form>
  );
}

Используя этот хук стоит учитывать одну вещь: надо проконтролировать, чтобы «дорогая» функция не пересоздавалась бы при каждом рендеринге компонента. Дело в том, что это приведёт к сбросу «замедленной» версии этой функции и сотрёт всё из её внутреннего состояния. Обеспечить вышеописанное требование можно двумя путями:

  1. Можно объявить «дорогую» функцию за пределами функционального компонента (так, как сделано в примере).
  2. Можно обернуть такую функцию с помощью хука useCallback.

Итоги


Существует немало хороших библиотек, в которых реализованы самые разные хуки. Если вас интересуют подобные библиотеки — можете начать знакомство с ними отсюда. Но, хотя в нашем распоряжении имеется множество полезных пользовательских хуков, хочу отметить, что те пять, о которых я рассказал — это те самые хуки, которые пригодятся в любом React-проекте.

Какими React-хуками вы пользуетесь чаще всего?