Пошук уроків, статей та іншого контенту
Розберете життєвий цикл запиту та відображення станів завантаження, помилки й успішної відповіді.
Запит до сервера не завершується миттєво. Поки застосунок очікує відповідь, користувач має розуміти, що процес триває. Так само потрібно показати повідомлення, якщо запит завершився помилкою або успішно повернув дані.
Найчастіше використовують такі стани:
idle — запит ще не розпочався;
loading — запит виконується;
success — відповідь успішно отримано;
error — під час запиту сталася помилка.
Ці стани утворюють життєвий цикл запиту:
idle → loading → success
↘ errorПісля повторної спроби стан знову переходить у loading.
Для запитів після відображення компонента зазвичай використовують useEffect. Стан відповіді зберігають у useState.
Приклад компонента, який завантажує завдання з сервера:
import { useEffect, useState } from "react";
export default function Todo() {
const [status, setStatus] = useState("idle");
const [todo, setTodo] = useState(null);
const [error, setError] = useState("");
const [attempt, setAttempt] = useState(0);
useEffect(() => {
const controller = new AbortController();
async function loadTodo() {
setStatus("loading");
setError("");
try {
const response = await fetch(
"https://jsonplaceholder.typicode.com/todos/1",
{
signal: controller.signal,
}
);
if (!response.ok) {
throw new Error(`HTTP error: ${response.status}`);
}
const data = await response.json();
setTodo(data);
setStatus("success");
} catch (requestError) {
// Скасований запит не потрібно показувати як помилку
if (requestError.name === "AbortError") {
return;
}
setError("Не вдалося завантажити завдання.");
setStatus("error");
}
}
loadTodo();
return () => {
controller.abort();
};
}, [attempt]);
if (status === "idle") {
return <p>Підготовка до завантаження...</p>;
}
if (status === "loading") {
return <p aria-live="polite">Завантаження...</p>;
}
if (status === "error") {
return (
<section>
<p role="alert">{error}</p>
<button onClick={() => setAttempt((value) => value + 1)}>
Спробувати ще раз
</button>
</section>
);
}
return (
<section>
<h2>{todo.title}</h2>
<p>Ідентифікатор: {todo.id}</p>
<p>{todo.completed ? "Завдання виконано" : "Завдання не виконано"}</p>
<button onClick={() => setAttempt((value) => value + 1)}>
Оновити
</button>
</section>
);
}Компонент створюється зі станом idle.
useEffect запускає функцію loadTodo.
Перед запитом стан змінюється на loading.
fetch надсилає запит до сервера.
Якщо відповідь успішна:
дані зберігаються в todo;
стан змінюється на success.
Якщо виникла помилка:
текст помилки зберігається в error;
стан змінюється на error.
Натискання кнопки повторної спроби збільшує attempt. Через це useEffect запускається ще раз.
У React компонент повторно рендериться після зміни стану. Це дає змогу показувати різний інтерфейс для кожної фази запиту.
Під час завантаження не варто показувати порожній екран:
if (status === "loading") {
return <p>Завантаження...</p>;
}Для довших запитів тут можна відобразити індикатор або текст із поясненням, але компонент не повинен створювати враження, що застосунок завис.
Помилка має бути зрозумілою користувачу:
if (status === "error") {
return (
<div>
<p role="alert">Не вдалося завантажити дані.</p>
<button onClick={retry}>Спробувати ще раз</button>
</div>
);
}Користувачу зазвичай не потрібно показувати технічний текст на кшталт TypeError: Failed to fetch. Такий текст можна залишити для журналу помилок, а в інтерфейсі показати коротке повідомлення.
Успішний стан потрібно перевіряти перед використанням даних:
if (status === "success") {
return <h2>{todo.title}</h2>;
}Не слід звертатися до todo.title, якщо todo ще дорівнює null.
fetch не вважає HTTP-помилки автоматично винятками. Наприклад, відповідь із кодом 404 або 500 все одно поверне об’єкт Response.
Тому потрібно перевіряти response.ok:
const response = await fetch("/api/todos/1");
if (!response.ok) {
throw new Error(`HTTP error: ${response.status}`);
}
const data = await response.json();Властивість response.ok має значення true для успішних HTTP-відповідей і false для помилок.
Компонент може бути видалений до того, як сервер відповість. Наприклад, користувач перейшов на іншу сторінку. Для скасування запиту можна використати AbortController:
useEffect(() => {
const controller = new AbortController();
fetch("/api/todos/1", {
signal: controller.signal,
});
return () => {
controller.abort();
};
}, []);Функція, яку повертає useEffect, називається функцією очищення. Вона викликається перед видаленням компонента або перед повторним запуском ефекту.
Після скасування fetch може завершитися помилкою з назвою AbortError. Це не обов’язково помилка для користувача, тому її зазвичай ігнорують.
Можна зберігати стан запиту окремими змінними:
const [isLoading, setIsLoading] = useState(false);
const [data, setData] = useState(null);
const [error, setError] = useState(null);Але в такому підході легко отримати суперечливі комбінації, наприклад:
isLoading === true одночасно з error;
data відсутні, але компонент намагається їх показати;
error залишилася після нового успішного запиту.
Рядок зі статусом допомагає описати поточну фазу явно:
const [status, setStatus] = useState("idle");Можна домовитися, що status завжди має одне з чотирьох значень:
"idle"
"loading"
"success"
"error"Це спрощує умовний рендеринг і робить логіку компонента передбачуванішою.
Неправильно:
return <h2>{todo.title}</h2>;На першому рендері todo ще може бути null, тому виникне помилка.
Правильно спочатку показати стан завантаження, а дані використовувати лише після успішної відповіді:
if (status === "loading") {
return <p>Завантаження...</p>;
}
if (status === "success") {
return <h2>{todo.title}</h2>;
}fetchНеправильно вважати будь-який отриманий об’єкт Response успішною відповіддю:
const response = await fetch("/api/data");
const data = await response.json();Потрібно перевіряти response.ok перед читанням даних.
Якщо після помилки розпочати новий запит, старе повідомлення може залишитися на екрані.
Перед новим запитом очистьте помилку:
setStatus("loading");
setError("");Порожній екран не пояснює користувачу, що відбувається. Для кожного стану має бути відповідний інтерфейс:
loading — повідомлення або індикатор завантаження;
error — пояснення проблеми та можливість повторити запит;
success — отримані дані.
Якщо компонент уже видалено, завершений запит не повинен намагатися оновити його стан. AbortController і перевірка AbortError допомагають безпечно завершувати такі запити.
Запит має життєвий цикл: idle, loading, success, error.
Стан завантаження потрібно показувати користувачу явно.
Помилки HTTP треба перевіряти через response.ok.
Дані можна відображати лише після успішного завершення запиту.
Для повторної спроби достатньо запустити запит ще раз і повернути стан у loading.
AbortController допомагає скасувати запит, коли компонент більше не потрібен.
Єдиний стан status часто робить логіку запиту зрозумілішою та надійнішою.