Що таке AbortController?
AbortController - браузерне Web API, яке створює AbortSignal для скасування fetch-запитів до того, як відповідь прийде.
Теорія
TL;DR
- Уяви пожежну тривогу: натиснути кнопку - fetch зупиняється до того, як відповідь досягне компонента
- Головне застосування: скасовувати API-запити при розмонтуванні React-компонента, щоб уникнути попереджень "setState on unmounted component"
controller.abort()синхронно генерує подію abort і закриває TCP-сокет- Правило: використовуй якщо запит займає понад 500ms або компонент може розмонтуватися раніше
- AbortError - це не мережева помилка. Завжди перевіряй
err.name !== 'AbortError'перед тим як показувати помилки користувачу
Швидкий приклад
const controller = new AbortController();
fetch('/api/user', { signal: controller.signal })
.then(res => res.json())
.then(data => console.log(data))
.catch(err => {
if (err.name === 'AbortError') console.log('Запит скасовано');
else console.error(err);
});
controller.abort(); // Виведе: "Запит скасовано"Передаємо signal у fetch - запит прив'язується до контролера. Викликаємо abort() - мережевий рівень закриває сокет. Жодної відповіді, жодного оновлення стану.
Ключова відмінність
До AbortController розробники використовували setTimeout щоб "скасовувати" запити. Таймаут наперед вгадує тривалість і витрачає трафік на швидкі відповіді, які вже не потрібні. AbortController скасовує в точний момент: коли ти вирішиш, а не коли спрацює таймер.
Коли використовувати
- Компонент розмонтовується до того, як API повернув відповідь: додай AbortController у cleanup useEffect
- Користувач вводить новий пошуковий запит до завершення попереднього: скасовуй старий
- Гонка між вкладками, що завантажують застарілі дані: abort старого fetch
- Пропускай для запитів до 100ms або для API, що не приймають AbortSignal (наприклад, setTimeout)
Як це працює всередині
При new AbortController() браузер створює AbortSignal з прапорцем aborted: false. Виклик controller.abort() перемикає прапорець на true і синхронно генерує подію 'abort'. fetch слухає цю подію і наказує мережевому рівню закрити TCP-сокет. У Node.js 15+ той самий механізм реалізований через undici HTTP-клієнт. Жодних полiфілів для сучасних середовищ не потрібно.
Типові помилки
Забути cleanup-функцію у useEffect
// Неправильно: немає abort при розмонтуванні
useEffect(() => {
const controller = new AbortController();
fetch('/api/data', { signal: controller.signal }).then(setData);
}, []);
// Немає return = fetch виконується після розмонтування, React друкує попередженняВиправлення: повертай () => controller.abort() з ефекту. Цей один рядок і є весь cleanup.
Перевикористання скасованого контролера
const controller = new AbortController();
controller.abort();
fetch('/api/new', { signal: controller.signal }); // Падає миттєвоСкасований signal залишається скасованим. fetch кидає AbortError ще до будь-якої мережевої активності. Цю помилку я бачив навіть у досвідчених розробників під час рефакторингу поспіхом. Завжди створюй new AbortController() для кожного запиту.
Обробляти AbortError як справжню помилку
// Неправильно: показує тост про помилку при навмисному скасуванні
fetch('/api').catch(err => showErrorToast(err));Виправлення: if (err.name !== 'AbortError') showErrorToast(err);
Вважати що axios автоматично підхоплює signal
Axios v1.6+ приймає signal у конфізі. Старіші версії ігнорують його без жодного попередження. Перевіряй версію або використовуй нативний fetch для цього патерну.
Де зустрічається в реальних проєктах
- React Query: автоматично передає
signalу query-функцію, скасовує при розмонтуванні компонента - SWR: передає AbortSignal у fetcher для stale-while-revalidate-скасувань
- RTK Query:
createApiскасовує запити коли компонент знищується - Express (Node.js):
res.setTimeout(5000, () => controller.abort())для зависших з'єднань AbortSignal.timeout(5000): статичний метод, що автоматично скасовує через заданий час без ручногоabort()
Типові питання на співбесіді
Q: Що станеться якщо викликати abort() після того, як відповідь вже прийшла?
A: Нічого. AbortController закриває сокет тільки до того, як відповідь буферизована. Після того як fetch отримав відповідь - скасування не має ефекту.
Q: Яка різниця між AbortSignal.timeout() і AbortController?
A: AbortSignal.timeout(5000) автоматично скасовується через 5 секунд без додаткового коду. AbortController - це ручний тригер: ти сам викликаєш abort() коли дія користувача або lifecycle компонента цього вимагають. Таймаут для простих часових обмежень, AbortController для всього іншого.
Q: Яка підтримка браузерів?
A: Chrome 66+, Firefox 57+, Safari 12.1+. Node.js 15+ підтримує нативно. Для старіших версій Node - пакет abort-controller з npm.
Q: Як скасувати кілька паралельних запитів одним контролером?
A: Передай той самий signal у всі fetch-виклики. Один abort() скасує їх усі. Але тоді ти втрачаєш незалежний контроль. Для окремого скасування - один контролер на запит.
Приклади
Базовий: скасування при розмонтуванні компонента
import { useEffect, useState } from 'react';
function ProductDetails({ productId }) {
const [product, setProduct] = useState(null);
useEffect(() => {
const controller = new AbortController();
fetch(`/api/products/${productId}`, { signal: controller.signal })
.then(res => res.json())
.then(setProduct)
.catch(err => {
if (err.name !== 'AbortError') console.error(err);
});
return () => controller.abort(); // Спрацює при розмонтуванні або зміні productId
}, [productId]);
return product ? <h1>{product.name}</h1> : <p>Завантаження...</p>;
}Коли productId змінюється, React запускає cleanup перед наступним ефектом. Cleanup викликає abort(), скасовуючи попередній запит. Стан оновлює тільки остання відповідь.
Середній: пошук із автоматичним скасуванням
import { useEffect, useState } from 'react';
function UserSearch({ query }) {
const [users, setUsers] = useState([]);
useEffect(() => {
if (!query) return;
const controller = new AbortController();
fetch(`/api/users?q=${query}`, { signal: controller.signal })
.then(res => res.json())
.then(setUsers)
.catch(err => {
if (err.name !== 'AbortError') console.error('Помилка пошуку:', err);
});
return () => controller.abort(); // Скасовуємо при наступному введенні
}, [query]);
return <ul>{users.map(u => <li key={u.id}>{u.name}</li>)}</ul>;
}
// Немає застарілих результатів, немає попередження "setState on unmounted component"Кожна зміна query запускає cleanup і скасовує попередній fetch. Новий ефект стартує з чистим контролером. Користувач завжди бачить результати для останнього введення, а не для якогось проміжного запиту.
Коротка відповідь
Для співбесідиКоротка відповідь допоможе вам впевнено відповідати на цю тему під час співбесіди.