Хочу рассказать о пяти простых 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>
);
}
Используя этот хук стоит учитывать одну вещь: надо проконтролировать, чтобы «дорогая» функция не пересоздавалась бы при каждом рендеринге компонента. Дело в том, что это приведёт к сбросу «замедленной» версии этой функции и сотрёт всё из её внутреннего состояния. Обеспечить вышеописанное требование можно двумя путями:
- Можно объявить «дорогую» функцию за пределами функционального компонента (так, как сделано в примере).
- Можно обернуть такую функцию с помощью хука useCallback.
Итоги
Существует немало хороших библиотек, в которых реализованы самые разные хуки. Если вас интересуют подобные библиотеки — можете начать знакомство с ними отсюда. Но, хотя в нашем распоряжении имеется множество полезных пользовательских хуков, хочу отметить, что те пять, о которых я рассказал — это те самые хуки, которые пригодятся в любом React-проекте.
Какими React-хуками вы пользуетесь чаще всего?
Автор: ru_vds