@ws-serenity/react-hooks
v3.5.0
Published
Some common hooks for React application
Maintainers
Keywords
Readme
Базовые хуки
useDebounce
Функция откладывает вызов коллбэка до того момента, когда с последнего вызова пройдёт определённое количество времени
const [ searchText, setSearchText ] = useState('');
const [ displayedParticipants, setDisplayedParticipants ] = useState(userParticipants);
// будет вызвана, когда ввод прекратится на SEARCH_INPUT_DEBOUNCE_DELAY милисекунд
const onSetDisplayedParticipants = (searchText: string) => {
setDisplayedParticipants(userParticipants.filter(user => user.person.displayName.includeIgnoringCase(searchText)));
};
const debounceFilter = useDebounce<string>(onSetDisplayedParticipants, SEARCH_INPUT_DEBOUNCE_DELAY);
useEffect(() => debounceFilter(searchText), [ searchText ]);useDebouncedValue
Возвращает значение, обновление которого отстаёт от переданного на указанное время. Откладывает обновление значения до
того момента, когда с последнего обновления значения пройдет указанное количество времени.
Удобно использовать в зависимостях useEffect, чтобы выполнять эффекты (например, запросы поиска) только после паузы во
вводе.
const [ searchText, setSearchText ] = useState('');
const debouncedSearchText = useDebouncedValue(searchText, SEARCH_INPUT_DEBOUNCE_DELAY);
useEffect(() => {
// будет вызвано, когда ввод прекратится на SEARCH_INPUT_DEBOUNCE_DELAY милисекунд
setDisplayedParticipants(userParticipants.filter(user => user.person.displayName.includeIgnoringCase(
debouncedSearchText)));
}, [ debouncedSearchText ]);useThrottle
Функция гарантирует, что переданный коллбэк будет вызываться не чаще, чем 1 раз в указанный интервал времени. Функция будет вызываться равномерно, первый вызов функции осуществится без задержек
// функция вывода позиции скролла без троттлинга
const handleScroll = (scrollTopPosition: number) => {
console.log('Scroll position:', scrollTopPosition);
}
// функция вызова с троттлингом, будет вызываться не чаще чем раз в SCROLL_THROTTLE_INTERVAL милисекунд
const throttledHandleScroll = useThrottle<number>(handleScroll, SCROLL_THROTTLE_INTERVAL);
return (
<div
// если пользователь быстро скроллит страницу, функция будет вызываться максимум раз в SCROLL_THROTTLE_INTERVAL мс,
// вместо сотен раз в секунду
onScroll={(e) => throttledHandleScroll(e.currentTarget.scrollTop)}
>
some scrollable block
</div>
)useThrottledValue
Возвращает значение, обновление которого происходит не чаще, чем 1 раз в указанный интервал времени. Обновление значения происходит равномерно, первое обновление происходит без задержек
const [ inputValue, setInputValue ] = useState('');
// будет обновляться равномерно, раз в INPUT_THROTTLE_DELAY мс
const throttledInputValue = useThrottledValue(inputValue, INPUT_THROTTLE_DELAY);
useEffect(() => {
// будет вызываться равномерно, раз в INPUT_THROTTLE_DELAY мс
// благодаря троттлингу будет вызываться не на каждый "чих", а с небольшой задержкой.
// но при этом не будет ждать, когда пользователь закончит печатать (как, например, при использовании useDebouncedValue),
// а будет плавно и равномерно обновлять значение раз в заданных интервал (может быть важно для условного превью)
setInputTextPreview(throttledInputValue);
}, [ throttledInputValue ]);
return (
<input
type={'text'}
value={inputValue}
onChange={(e) => setInputValue(e.target.value)}
placeholder={'Введите развернутый ответ...'}
/>
)useOutsideClick
Колбэк вызывается, когда происходит нажатие за пределами ref указанного компонента.
Хук поддерживает два варианта использования: Вариант 1: хук создает и возвращает ref на элемент, за которым нужно следить
const [ isMenuOpen, setIsMenuOpen ] = useState(false);
const menuRef = useOutsideClick<HTMLDivElement>(() => setIsMenuOpened(false));
return <div ref={menuRef}>Menu content</div>;Вариант 2: передача существующего ref
const [ isMenuOpen, setIsMenuOpen ] = useState(false);
// полезно, когда ref элемента используется для нескольких целей:
const menuRef = useRef<HTMLDivElement>(null);
// хук принимает существующий ref и ничего не возвращает
useOutsideClick(() => setIsMenuOpened(false), menuRef);
return <div ref={menuRef}>Menu content</div>useResizeObserver
Следит за изменением размера любого HTML-элемента, при изменении вызывает колбэк. Возвращает ref, за обновлением элемента которого будет следить.
const updateSizeCount = useCallback(() => {
// обновление стейта работает, но для этого неоходим дополнительный объект с постоянной ссылкой, чтобы замыкание сработало на него
sizeChangedCountRef.current = sizeChangedCountRef.current + 1;
setSizeChangedCount(sizeChangedCountRef.current);
}, []);
// так делать НЕ НАДО
const WRONG_updateSizeCount = useCallback(() => {
// в противном случае значение стейта - значимого типа (sizeChangedCount) - всегда будет оставаться прежним, потому что замкнется
setSizeChangedCount(sizeChangedCount + 1);
}, []);
const containerRef = useResizeObserver<HTMLUListElement>(updateSizeCount);useTouchHold
Предназначен для вызова колбэка при удержании элемента на мобильном и планшетном устройстве
function SomeComponent() {
const ref = useTouchHold<HTMLDivElement>(onOpenParticipantsModal, OPEN_PARTICIPANTS_MODAL_DELAY);
return (
<div
className={'perticipant'}
ref={ref}
>
</div>
)
}useKeyPress
Позволяет добавлять хэндлеры для нажатия определенных клавиш.
useKeyPress({
keyPressParams: {
Escape: {
onKeyDown: () => setMessage('There\'s no escape from simulation'),
},
ArrowUp: {
onKeyDown: () => {
},
},
ArrowLeft: {
onKeyDown: () => {
},
},
ArrowDown: {
onKeyDown: () => {
},
},
ArrowRight: {
onKeyDown: () => {
},
},
},
options: { passive: true },
});| Параметр | Тип | По умолчанию | Описание |
|----------------|---------------------------|---------------------|----------------------------------------------------|
| keyPressParams | UseKeyPressParams | | Объект, описывающий обработчики для каждого ключа. |
| target | RefObject<HTMLElement> | | Элемент, на котором необходимо отловить событие. |
| options | AddEventListenerOptions | { capture: true } | Опции стандартного EventListner'а. |
useFileSelect
Вызывает API браузера для выбора файла.
import { useFileSelect } from '@ws-serenity/react-hooks';
const selectFile = useFileSelect({
handleSelect: () => { /* ... */
},
multiple: true,
accept: {
extensions: [ 'ftl', 'gtl' ],
},
});
selectFile();| Параметр | Тип | По умолчанию | Описание |
|--------------|---------------------------|--------------|-------------------------------------------------------------------------|
| handleSelect | (files: File[]) => void | | Обработчик для выбранных файлов. |
| multiple | boolean | false | Разрешить выбирать несколько файлов. |
| accept | AcceptConfig | | Задает параметры для фильтрации файлов по их расширению и их mime type. |
export type AcceptConfig = {
// Регистр не учитывается
// Должны быть заданы без точки
// [ '.pdf', '.txt' ] - ⚠️ неправильно
// [ 'pdf', 'PDF', 'txt', 'TXT' ] - ⚠️ допустимо
// [ 'pdf', 'txt' ] - ✅ правильно
extensions?: string[],
// Регистр не учитывается
// Будет расширено до обобщения images -> images/*
// [ 'image/jpeg', 'image/png' ... ] - ⚠️ неправильно
// [ 'IMAGE', 'VIDEO' ] - ⚠️ допустимо, но с нарушением name convention
// [ 'image', 'video' ] - ✅ правильно
mimeTypes?: string[],
}useDropZone
Отслеживает перетаскивание файлов в контейнер.
Также, может обработать клик на контейнер, вызвав browser api для выбора файла.
Возвращает флаг isActive, коллекцию listeners и метод selectFiles.
import { useDropZone } from '@ws-serenity/react-hooks';
const {
isActive,
listeners,
selectFiles
} = useDropZone();
/* ... */
<div
className={isActive ? 'drop-zone drop-zone--active' : 'drop-zone'}
{...listeners}
>
{/* ... */}
</div>
<button onClick={selectFiles}>Add files</button>| Возврат | Тип | Описание |
|-----------------|--------------|--------------------------------------------------------------------------------------|
| isActive | boolean | true если перетаскиваемый объект находится в зоне контейнера, false иначе. |
| isWindowsActive | boolean | true, если перетаскиваемый объект находится в зоне окна, false иначе. |
| listeners | Listeners | Коллекция listeners на которые нужно подписать контейнер для корректной работы хука. |
| selectFiles | () => void | Вызывает browser api для выбора файла, игнорирует dropOnly флаг. |
| Параметр | Тип | По умолчанию | Описание |
|------------|---------------------------|--------------|--------------------------------------------------------------------------------------------------------------|
| handleDrop | (files: File[]) => void | | Обработчик для выбранных файлов. |
| dropOnly | boolean | false | Если true, то файлы буду добавляться только перетаскиванием. |
| filterDrop | boolean | true | Если false, то файлы, добавленные перетаскиванием, не будут проверяться на соответствие условиям accept. |
| disabled | boolean | false | Если true, то работа хука приостанавливается. |
| multiple | boolean | true | Разрешить выбирать несколько файлов. |
| accept | AcceptConfig | | Задает параметры для фильтрации файлов по их расширению и их mime type. |
AcceptConfig - см. useFileSelect.AcceptConfig.
useCallbackRef
Хук, объединяющий cb = useCallback( /* ... */ ) и cbRef = useRef(cb).
import { useCallback } from "react";
import { useCallbackRef } from "@ws-serenity/react-hooks";
const callback = useCallback(() => { /* ... */
}, []);
const [ cb, cbRef ] = useCallbackRef(() => { /* ... */
}, []);Мотивация - невозможность обновить анонимную функцию, использующую коллбэк, например:
import { useCallback } from "react";
type AirType = 'normal' | 'discharged';
const air: AirType = 'normal';
const breathe = useCallback(() => { /* ... */
}, [ air ]);
const human = {
/* ... */
breathe: async () => breathe()
}В примере выше, метод human.breathe() не обновится вместе с breathe.
Чтобы это исправить, можно использовать ссылку на коллбэк.
const [ breathe, breatheRef ] = useCallbackRef(() => { /* ... */
}, [ air ]);
const human = { /* ... */ breathe: async () => breatheRef.current() }Также, этот хук позволяет использовать некоторые оптимизации мемоизации, позволяя не подписываться на изменение коллбэка.
useLongPolling
Универсальный хук для длинного опроса сервера (long polling). Не привязан к конкретному HTTP-клиенту (axios/fetch). Запросы выполняются строго последовательно, поддерживается отмена через AbortController (signal передаётся в fetchFn).
/* using axios http client */
const { data, isLoading, error, startPolling, stopPolling } = useLongPolling<DataItem[], AxiosResponse<DataItem[]>>({
fetchFn: (signal: AbortSignal) => {
const params: Params = {
lastItemId: current.id,
}
return api.getPollingData(params, signal);
},
transform: (response) => response.data,
onSuccess: (data) => { /* может быть async */
/* обновление локального стейта и т.д. */
},
enabled: isLongLoppingEnabled,
});
/* можно использовать просто, если возвращаемые параметры не нужны */
useLongPolling<DataItem[], AxiosResponse<DataItem[]>>({
...
});| Параметр | Тип | По умолчанию | Описание |
|-----------------------|-----------------------------------------------|--------------|---------------------------------------------------------------------------------|
| fetchFn | (signal: AbortSignal) => Promise<TResponse> | | Функция запроса, передаем signal используемый в HTTP-клиент для отмены |
| transform | (response: TResponse) => TData | | Опциональная трансформация ответа (e.g., response => response.data для axios) |
| onSuccess | (data: TData) => void ? Promise<void> | | Колбэк при успешном завершении запроса |
| onError | (error: unknown) => void | | Колбэк при ошибке |
| onFinally | () => void | | Колбэк после каждой итерации |
| enabled | boolean | true | Флаг активности long polling'а |
| shouldContinueOnError | boolean | true | Продолжать ли опрос после ошибки (по умолчанию true) |
| Возврат | Тип | Описание |
|--------------|----------------|--------------------------------------------------------------------------|
| data | TData ? null | Последние успешные данные TData |
| isLoading | boolean | true, если перетаскиваемый объект находится в зоне окна, false иначе |
| error | unknown | Ошибка (не abort запроса) |
| startPolling | () => void | Ручной запуск polling |
| stopPolling | () => void | Ручная остановка polling + abort текущего запроса |
useClipboard
Обработчик для различных событий буфера обмена
import { useClipboard } from '@ws-serenity/react-hooks';
useClipboard({
onPasteFiles: (files: File[]) => console.log(files),
});initEsiaAuth
Функция для упрощения авторизации через ESIA, является инициализирующей функцией и ее ЗАПРЕЩАЕТСЯ использовать внутри компонента, потому что функция возвращает 2 хука - один для начала авторизации, второй для ее завершения.
Авторизацию можно легко доработать до "любой сторонней авторизации", например, до GoogleAuth, но пока не требуется
// инициализация хуков в отдельном модуле
import { initEsiaAuth } from '@ws-serenity/react-hooks';
const [
useAuthEsiaInit,
useAuthEsiaComplete,
] = initEsiaAuth(
`${apiUrl}/auth-service/esia/init?redirectUrl=https://${window.location.hostname}/auth/external/esia`,
);
export { useAuthEsiaInit, useAuthEsiaComplete };
// компонент вызова esia, с которым взаимодействует пользователь
export function AuthEsiaButton() {
// передаем метод авторизации, который необходимо вызвать, мы не можем перенести функцию в init,
// чтобы обеспечить совместимость с другими хуками, а не только с глобальными функциями
const start = useAuthEsiaInit((dto) => signIn('auth-esia', { ...dto, redirect: false }));
return (
<button onClick={start}>Войти через ESIA</button>
);
}
// отдельная страница, на которую мы получим редирект после успешной авторизации
// auth/[code]/esia
export default function EsiaAuthPage({ code }: EsiaAuthPageProps) {
// сюда можно передать необходимые данные любым способом
useAuthEsiaComplete(code, [ code ]);
return (<AppLoader/>);
} useIosFriendlyClick
По какой-то причине просто onclick не срабатывает в некоторых случаях в Web на iOS
С такой проблемой столкнулись в списке опций селекта на ios. Баг воспроизводился во всех браузерах iOS
Советы из интернетов не помогли (https://stackoverflow.com/questions/24077725/mobile-safari-sometimes-does-not-trigger-the-click-event) поэтому было состряпано собственное решение
При использовании хука нет необходимости вручную навешивать onClick
