useAsyncCallback
Описание
useAsyncCallback — хук‑обёртка над асинхронной функцией, который отслеживает её статус ('pending' / 'success' / 'error'), поддерживает ленивую реактивность (перерисовки начинаются только после первого обращения к status) и защищает от гонок: только последний вызов влияет на статус. Предоставляет методы: handler (вызов асинхронной операции) и abort() (сброс/отмена актуального вызова).
Возвращаемая структура — гибрид для кортежной и объектной деструктуризации: [handler, status, abort] и { handler, status, abort }.
Сигнатура
function useAsyncCallback<Args extends unknown[], R>(
asyncCallback: (...args: Args) => Promise<R>,
): UseAsyncCallbackReturn<Args, R>;Параметры
asyncCallback— асинхронная функция, статус выполнения которой нужно отслеживать.
Возвращает:
UseAsyncCallbackReturn<Args, R>— гибридная структура:handler(...args): Promise<R>— обёртка над исходной функцией;status— флагиisPending,isSuccess,isErrorиerror(только при ошибке);abort()— отменяет актуальный вызов и переводит статус в'initial'.
Примеры
1) Кнопка «Сохранить»: блокировка и показ ошибок
import { useAsyncCallback } from '@webeach/react-hooks';
type SaveButtonProps = {
save: () => Promise<void>;
};
export function SaveButton(props: SaveButtonProps) {
const { save } = props;
const { handler: onSave, status, abort } = useAsyncCallback(save);
return (
<>
<button onClick={() => onSave()} disabled={status.isPending}>
{status.isPending ? 'Saving…' : 'Save'}
</button>
{status.isError && status.error !== null && (
<div role="alert">{status.error.message}</div>
)}
{/* При необходимости дать пользователю отмену */}
{status.isPending && (
<button onClick={abort} type="button">
Cancel
</button>
)}
</>
);
}2) Использование без реактивности (не читаем status)
const [submit] = useAsyncCallback(async (form: FormData) => {
await api.submit(form);
});
// Компонент не будет перерисовываться при смене статуса,
// т.к. `status` ни разу не прочитан. При необходимости — await/try-catch.3) Поиск с отменой предыдущего запроса
import { ChangeEventHandler } from 'react';
import { useAsyncCallback } from '@webeach/react-hooks';
export function SearchBox() {
const {
handler: load,
status,
abort,
} = useAsyncCallback(async (q: string) => {
const response = await fetch(`/api/search?q=${encodeURIComponent(q)}`);
if (!response.ok) {
throw new Error('Network error');
}
return res.json();
});
const handleInputChange: ChangeEventHandler<HTMLInputElement> = (event) => {
abort(); // отменяем прошлый запрос логически
void load(event.target.value); // запускаем новый
};
return (
<div>
<input onChange={handleInputChange} placeholder="Search…" />
{status.isPending && <span>Loading…</span>}
{status.isError && <span role="alert">{status.error?.message}</span>}
</div>
);
}Поведение
Ленивая реактивность
- Пока к полю
statusне обращались, изменения статуса не вызывают перерисовку. При первом чтенииstatusреактивность включается автоматически, и далее обновления будут отражаться в UI.
- Пока к полю
Защита от гонок
- Если
handlerвызывается повторно до завершения предыдущего вызова, только последний вызов сможет изменить статус. Завершения «устаревших» вызовов игнорируются.
- Если
Статусы и ошибка
- Доступны флаги
isPending,isSuccess,isErrorиerror(ErrorLike | null). Если выброшено значение не форматаErrorLike, хук попытается привести его к корректному виду (string→Error).
- Доступны флаги
abort()- Сбрасывает статус в
'initial'и помечает текущий вызов как отменённый. Сетевые запросы при этом не прерываются физически — используйтеAbortControllerвнутриasyncCallback, если нужна реальная отмена I/O.
- Сбрасывает статус в
Поведение при размонтировании
- При размонтировании компонента выполняется
abort()автоматически, чтобы избежать гонок и утечек состояния.
- При размонтировании компонента выполняется
Гибридная структура
- Результат доступен и как кортеж
[handler, status, abort], и как объект{ handler, status, abort }— выбирайте форму под свой стиль кода.
- Результат доступен и как кортеж
Когда использовать
- Обёртка над сетевыми запросами/асинхронными операциями с управлением статуса.
- Блокировка UI на время запроса (
disabled={status.isPending}), показ ошибок и индикаций. - Сценарии, где важна защита от гонок при множественных последовательных вызовах.
Когда не использовать
- Если хватает прямого
useState+useCallbackбез статусов. - Если нужна интеграция с внешним менеджером состояний/запросов (React Query и др.) — используйте их возможности.
- Если обязателен «жёсткий» cancel I/O — стройте отмену внутри
asyncCallbackчерезAbortController/сигналы.
Частые ошибки
Ожидание перерисовок без чтения
status- Если
statusне используется, перерисовок не будет. Либо читайтеstatus, либо управляйте UI черезawait/try-catch/внешнее состояние.
- Если
Игнорирование выброшенной ошибки
handlerпробрасывает ошибку вызова. Оборачивайте вtry/catch, если не используетеstatus.isError.
Путаница с «отменой»
abort()не отменяет сетевой запрос сам по себе — только сбрасывает внутренний статус и «аннулирует» результат текущего вызова.
Случайная гонка из‑за параллельных вызовов
- Не запускайте конкурирующие вызовы без необходимости. Если это ожидаемо, UI должен учитывать, что статус отображает последний вызов.
Типизация
Экспортируемые типы
UseAsyncCallbackReturn<Args, ReturnType>- Гибридный тип результата: кортеж
[handler, status, abort]и объект{ handler, status, abort }.
- Гибридный тип результата: кортеж
UseAsyncCallbackReturnObject<Args, ReturnType>- Объектная форма результата с полями
handler,status,abort.
- Объектная форма результата с полями
UseAsyncCallbackReturnTuple<Args, ReturnType>- Кортежная форма:
[handler: (...args: Args) => Promise<R>, status: StatusStateMapTuple & StatusStateMap, abort: () => void].
- Кортежная форма:
UseAsyncCallbackAbortHandler- Сигнатура функции отмены:
() => void.
- Сигнатура функции отмены: