Skip to main content

Що таке AbortController?

AbortController - браузерне Web API, яке створює AbortSignal для скасування fetch-запитів до того, як відповідь прийде.

Теорія

TL;DR

  • Уяви пожежну тривогу: натиснути кнопку - fetch зупиняється до того, як відповідь досягне компонента
  • Головне застосування: скасовувати API-запити при розмонтуванні React-компонента, щоб уникнути попереджень "setState on unmounted component"
  • controller.abort() синхронно генерує подію abort і закриває TCP-сокет
  • Правило: використовуй якщо запит займає понад 500ms або компонент може розмонтуватися раніше
  • AbortError - це не мережева помилка. Завжди перевіряй err.name !== 'AbortError' перед тим як показувати помилки користувачу

Швидкий приклад

javascript
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

javascript
// Неправильно: немає abort при розмонтуванні useEffect(() => { const controller = new AbortController(); fetch('/api/data', { signal: controller.signal }).then(setData); }, []); // Немає return = fetch виконується після розмонтування, React друкує попередження

Виправлення: повертай () => controller.abort() з ефекту. Цей один рядок і є весь cleanup.

Перевикористання скасованого контролера

javascript
const controller = new AbortController(); controller.abort(); fetch('/api/new', { signal: controller.signal }); // Падає миттєво

Скасований signal залишається скасованим. fetch кидає AbortError ще до будь-якої мережевої активності. Цю помилку я бачив навіть у досвідчених розробників під час рефакторингу поспіхом. Завжди створюй new AbortController() для кожного запиту.

Обробляти AbortError як справжню помилку

javascript
// Неправильно: показує тост про помилку при навмисному скасуванні 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() скасує їх усі. Але тоді ти втрачаєш незалежний контроль. Для окремого скасування - один контролер на запит.

Приклади

Базовий: скасування при розмонтуванні компонента

javascript
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(), скасовуючи попередній запит. Стан оновлює тільки остання відповідь.

Середній: пошук із автоматичним скасуванням

javascript
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. Новий ефект стартує з чистим контролером. Користувач завжди бачить результати для останнього введення, а не для якогось проміжного запиту.

Коротка відповідь

Для співбесіди
Premium

Коротка відповідь допоможе вам впевнено відповідати на цю тему під час співбесіди.

Дочитали статтю?