Запропонувати правкуПокращити цю статтюДопрацюйте відповідь до «Що таке AbortController?». Ваші зміни проходять модерацію перед публікацією.Потрібне підтвердженняКонтентЩо ви змінюєте🇺🇸EN🇺🇦UAПереглядЗаголовок (UA)Коротка відповідь (UA)**AbortController** - браузерне API, яке створює signal для скасування fetch-запитів. ```javascript const controller = new AbortController(); fetch('/api/data', { signal: controller.signal }); controller.abort(); // Скасовує запит, кидає AbortError ``` **Ключове:** завжди створюй новий AbortController для кожного запиту. Скасований signal залишається скасованим і миттєво падає при повторному використанні.Показується над повною відповіддю для швидкого нагадування.Відповідь (UA)Зображення**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. Новий ефект стартує з чистим контролером. Користувач завжди бачить результати для останнього введення, а не для якогось проміжного запиту.Для рев’юераПримітка для модератора (необов’язково)Бачить лише модератор. Прискорює рев’ю.