Пошук уроків, статей та іншого контенту
Застосуєте AbortController, щоб скасовувати запити та запобігати оновленню стану після розмонтування.
Асинхронний запит може завершитися після того, як компонент уже розмонтували. Наприклад:
компонент запускає fetch;
користувач переходить на іншу сторінку;
компонент розмонтовується;
сервер повертає відповідь;
обробник запиту намагається оновити стан уже неіснуючого компонента.
Це може спричинити зайву роботу, некоректні дані або попередження React.
AbortController дає змогу скасувати операцію, яка підтримує AbortSignal. Метод fetch підтримує цей механізм.
Основна послідовність:
створити AbortController;
передати його signal у fetch;
викликати controller.abort() у функції очищення useEffect;
окремо обробити помилку скасування.
const controller = new AbortController();
fetch("/api/users", {
signal: controller.signal,
})
.then((response) => response.json())
.then((users) => {
console.log(users);
})
.catch((error) => {
if (error.name === "AbortError") {
console.log("Запит скасовано");
return;
}
console.error("Помилка запиту:", error);
});
// Пізніше запит можна скасувати
controller.abort();Після виклику abort() проміс fetch буде відхилено. Для скасованого запиту зазвичай використовується помилка з name === "AbortError".
Скасування запиту на клієнті не гарантує, що сервер припинить виконувати обробку. Воно припиняє очікування відповіді в браузері та не дає продовжити обробку результату на клієнті.
Контролер потрібно створювати всередині конкретного запуску ефекту. У функції очищення ефекту запит скасовується:
import { useEffect, useState } from "react";
function UserPosts({ userId }) {
const [posts, setPosts] = useState([]);
const [status, setStatus] = useState("idle");
const [error, setError] = useState("");
useEffect(() => {
const controller = new AbortController();
async function loadPosts() {
setStatus("loading");
setError("");
try {
const response = await fetch(
`https://jsonplaceholder.typicode.com/posts?userId=${userId}`,
{
signal: controller.signal,
}
);
if (!response.ok) {
throw new Error(`HTTP-помилка: ${response.status}`);
}
const data = await response.json();
// Якщо запит не скасовано, результат можна записати у стан
setPosts(data);
setStatus("success");
} catch (error) {
if (error.name === "AbortError") {
// Скасування є очікуваною ситуацією, а не помилкою інтерфейсу
return;
}
setError(error instanceof Error ? error.message : "Невідома помилка");
setStatus("error");
}
}
loadPosts();
return () => {
// Виконується перед повторним запуском ефекту
// або перед розмонтуванням компонента
controller.abort();
};
}, [userId]);
if (status === "loading") {
return <p>Завантаження...</p>;
}
if (status === "error") {
return <p>Помилка: {error}</p>;
}
return (
<ul>
{posts.map((post) => (
<li key={post.id}>{post.title}</li>
))}
</ul>
);
}
export default UserPosts;Функція, яку повертає useEffect, виконується:
перед повторним запуском ефекту, якщо змінилася залежність;
під час розмонтування компонента;
у режимі розробки Strict Mode React може додатково запускати й очищати ефект для перевірки коректності.
Тому, якщо userId змінюється, React спочатку скасує запит для попереднього користувача, а потім запустить новий ефект для нового userId.
У цьому прикладі кнопка дає змогу розмонтувати компонент під час запиту:
import { useState, useEffect } from "react";
import { createRoot } from "react-dom/client";
function UserPosts({ userId }) {
const [posts, setPosts] = useState([]);
const [status, setStatus] = useState("loading");
const [error, setError] = useState("");
useEffect(() => {
const controller = new AbortController();
async function loadPosts() {
try {
const response = await fetch(
`https://jsonplaceholder.typicode.com/posts?userId=${userId}`,
{
signal: controller.signal,
}
);
if (!response.ok) {
throw new Error(`HTTP-помилка: ${response.status}`);
}
const data = await response.json();
setPosts(data);
setStatus("success");
} catch (error) {
if (error.name === "AbortError") {
// Компонент більше не потребує результату цього запиту
return;
}
setError(error instanceof Error ? error.message : "Невідома помилка");
setStatus("error");
}
}
loadPosts();
return () => {
controller.abort();
};
}, [userId]);
if (status === "loading") {
return <p>Завантаження дописів...</p>;
}
if (status === "error") {
return <p>Не вдалося завантажити дописи: {error}</p>;
}
return (
<section>
<h2>Дописи користувача {userId}</h2>
<ul>
{posts.map((post) => (
<li key={post.id}>{post.title}</li>
))}
</ul>
</section>
);
}
function App() {
const [userId, setUserId] = useState(1);
const [showPosts, setShowPosts] = useState(true);
return (
<main>
<label>
Користувач:{" "}
<select
value={userId}
onChange={(event) => setUserId(event.target.value)}
>
<option value="1">1</option>
<option value="2">2</option>
<option value="3">3</option>
</select>
</label>
<button onClick={() => setShowPosts((value) => !value)}>
{showPosts ? "Сховати дописи" : "Показати дописи"}
</button>
{showPosts && <UserPosts userId={userId} />}
</main>
);
}
createRoot(document.getElementById("root")).render(<App />);Коли UserPosts зникає з дерева компонентів, React викликає очищення ефекту, а controller.abort() скасовує активний запит.
Коли змінюється userId, старий компонентний ефект також очищується. Це запобігає ситуації, у якій повільна відповідь для попереднього користувача приходить після швидшої відповіді для нового користувача.
У більш складній логіці можна явно перевірити, чи був скасований сигнал:
useEffect(() => {
const controller = new AbortController();
async function loadData() {
try {
const response = await fetch("/api/data", {
signal: controller.signal,
});
const data = await response.json();
if (controller.signal.aborted) {
return;
}
setData(data);
} catch (error) {
if (controller.signal.aborted) {
return;
}
setError(error);
}
}
loadData();
return () => {
controller.abort();
};
}, []);У більшості випадків достатньо обробки AbortError у catch. Перевірка signal.aborted може бути корисною, якщо між отриманням результату та оновленням стану є додаткова асинхронна логіка.
Контролер після abort() залишається скасованим. Його не можна повторно використати для нового запиту:
const controller = new AbortController();
controller.abort();
// Цей запит одразу буде скасовано
fetch("/api/data", {
signal: controller.signal,
});У useEffect новий контролер створюється під час кожного запуску ефекту, тому ця проблема не виникає.
Контролер сам по собі не скасовує запит. Сигнал потрібно передати в параметрах fetch:
fetch("/api/data", {
signal: controller.signal,
});Без signal виклик controller.abort() не вплине на запит.
Скасування під час розмонтування або зміни параметрів є очікуваною поведінкою. Не варто показувати користувачу повідомлення «Помилка завантаження», якщо запит був скасований навмисно.
catch (error) {
if (error.name === "AbortError") {
return;
}
setError("Не вдалося завантажити дані");
}Контролер, пов'язаний з одним компонентом або запитом, краще створювати безпосередньо в ефекті. Це допомагає уникнути скасування нового запиту старим очищенням.
useEffect(() => {
fetch("/api/data")
.then((response) => response.json())
.then((data) => setData(data));
}, []);Такий код не має способу скасувати запит під час розмонтування.
Правильний варіант повинен створювати контролер і повертати функцію очищення:
useEffect(() => {
const controller = new AbortController();
fetch("/api/data", {
signal: controller.signal,
});
return () => {
controller.abort();
};
}, []);Якщо URL або параметр запиту залежить від userId, він має бути в масиві залежностей:
useEffect(() => {
// Запит використовує userId
}, [userId]);Якщо залишити [], ефект використає лише початкове значення userId і не запустить новий запит після його зміни.
catch (error) {
setError("Запит не виконався");
}Такий код показуватиме помилку навіть тоді, коли запит скасовано навмисно. Перевіряйте error.name перед оновленням стану помилки.
useEffect(() => {
const controller = new AbortController();
return () => {
controller.abort();
};
fetch("/api/data", {
signal: controller.signal,
});
}, []);Код після return недосяжний, тому запит не буде запущено. Спочатку потрібно виконати асинхронну операцію, а потім повернути cleanup.
AbortController використовується для скасування операцій із підтримкою AbortSignal.
Для fetch сигнал передається через параметр signal.
У React контролер створюють усередині useEffect.
controller.abort() викликають у функції очищення ефекту.
Очищення виконується під час розмонтування та перед повторним запуском ефекту.
Помилку з name === "AbortError" потрібно обробляти окремо.
Новий запит повинен отримувати новий екземпляр AbortController.
Скасування запитів допомагає не обробляти застарілі відповіді та не оновлювати стан після розмонтування компонента.