Загружай удалённые данные через fetch внутри эффекта, аккуратно моделируя состояния загрузки и ошибки.
Открыть этот урок в KodokonСетевой запрос занимает время и может провалиться: твой интерфейс должен отражать эту реальность. Профессиональное соглашение - отслеживать три отдельных состояния: полученные данные, булев флаг загрузки и возможную ошибку. Сам запрос идёт внутрь useEffect, потому что обращение к серверу - это побочный эффект.
const [users, setUsers] = useState([]);
const [loading, setLoading] = useState(true);
const [error, setError] = useState(null);
useEffect(() => {
fetch("https://api.example.com/users")
.then((res) => {
if (!res.ok) throw new Error("HTTP " + res.status);
return res.json();
})
.then((data) => setUsers(data))
.catch((err) => setError(err.message))
.finally(() => setLoading(false));
}, []);В рендере обрабатывай случаи по порядку: сначала загрузка, затем ошибка и в конце данные. Эти ранние возвраты сохраняют код читаемым и гарантируют, что пользователь всегда видит что-то осмысленное: индикатор ожидания, сообщение о сбое или ожидаемый список.
if (loading) return <p>Loading…</p>;
if (error) return <p>Error: {error}</p>;
return (
<ul>
{users.map((user) => (
<li key={user.id}>{user.name}</li>
))}
</ul>
);Последняя ловушка: если зависимость меняется (например, userId), уходит новый запрос, тогда как старый может быть ещё в пути. Если старый ответит последним, он перезапишет свежие данные. Классическое противоядие: флаг ignore, который функция очистки ставит в true, чтобы выбрасывать устаревшие ответы.
useEffect(() => {
let ignore = false;
setLoading(true);
fetch("/api/users/" + userId)
.then((res) => res.json())
.then((data) => {
if (!ignore) setUser(data);
})
.finally(() => {
if (!ignore) setLoading(false);
});
return () => {
ignore = true;
};
}, [userId]);catch так и не срабатывает. Почему?ignore, который очистка эффекта ставит в true?