Хук 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-хуками вы пользуетесь чаще всего?
justboris
В useAsync забыли про то что компонент может размонтироваться во время операции:
В библиотеке, на которую ссылается автор этот момент уже предусмотрен.
В хуке useDebounce тоже заложили грабли. Вот такой код работать не будет:
У автора на эту тему есть оговорка, что функция не должна пересоздаваться, но на самом деле это можно поправить внутри хука, и не заморачивать пользователя этими деталями
Таким образом и получается, то даже в простых штуках есть неочевидные моменты, о которых нужно знать.
Finesse
Зачем в
useDebounce
используетсяuseEffect
? Потому что так написано в документации React в примере сuseEventCallback
? Это вызывало проблемы в моём проекте, я пытался придумать пример, в которомuseEffect
необходим, но не смог. Проще и надёжнее оказалось писать в ref незамедлительно:justboris
Во-первых, если так написано в документации, то это неспроста. Команде реакта виднее.
Во-вторых, могу сходу придумать два варианта, чем это может быть полезно – 1) этот код не вызывается на сервере 2) это нужно для ConcurrentMode, где рендер может вызываться много раз
А какие проблемы это вызывает?
strannik_k
В данном случае (для использования debounceAction) ошибки вроде не будет, но в других ситуациях использования useRef в useEffect и в useMemo, вариант с useEffect может быть некорректен, т.к. useEffect вызывается после useMemo из-за чего ref не успеет обновится и useMemo выполнится с предыдущим переданным значением.
Finesse
useCallback
иuseEventCallback
— это отпимизация простой записи функции в переменную. То есть, следующие примеры должны быть эквивалентны с точки зрения логики работы компонента:Но на самом деле
useEventCallback
(предлагаемый в документации) работает не так, как другие варианты.Они позиционируют это как антипример, поэтому отношение соответствующее.
Та функция будет объявлена в обоих случаях и не будет вызвана тоже в обоих случаях.
Запись в ref проще для JS-интерпретатора, чем вызов функции
useEffect
. Сайд эффекты не производятся. Поэтому не вижу проблемы с точки зрения множественного рендера.strannik_k привёл хороший пример. Когда функция, которую возвращает
useEventCallback
вызывается сразу же в процессе рендера (например, в ответ на изменение одного из пропов), получается баг.faiwer
Не модифицируйте
ref
-ы во времяrender
-а. Они предполагаются быть чистыми (pure functions
). В противном случае вы можете столкнуться с гхм… со странностями.Например при активной вкладке с
react dev tools
. Он (пока вы там кликаете по компонентам) рендерит их с ненастоящими хуками (и таким образом строит древо используемых хуков). По итогу получается что если вы, скажем, сохранили вref.current
ссылку на какой-нибудь, скажем,setValue
изuseState
, то теперь у вас там просто пустая болванка из-заreact-dev-tools
. И когда этотref.current()
где-нибудь будет вызван, то ничего не произойдёт (там() => {}
). А вы будете ломать голову — в чём же баг?!Полагаю это не единственный возможный случай. И судя по валидатору от React-а они это пишут в документации не с проста. У них там всякие грандиозные планы (вроде рендера в пустоту). Что-нибудь потом да сломается.
В общем используйте
useLayoutEffect
Finesse
Аргумент про то, что render должен быть чистой функцией, звучит убедительно. Можно, пожалуйста, какой-нибудь конкретный пример с записью в ref во время рендера, который можно запустить и сломать с помощью React dev tools?
faiwer
Вот тут мы это обсуждали. Там же в обсуждениях и ссылка на issue на github-е с разъяснениями.
Alexandroppolus
А вот, к примеру, МобХ делает свои точечные подписки как раз во время рендера. Тут есть проблема? Или он как-то "коммитится" в useLayoutEffect?
faiwer
Честно говоря понятия не имею. Думаю проще всего — просто открыть исходники mobX и посмотреть. Плюс стоит учесть, что раз мы говорим про хуки, то мы говорим про функциональные компоненты, а не про классы. У них своя специфика. Полагаю к рендеру классовых компонент меньше требований.