Загрузка данных и loading states
Loading states — явные состояния UI (загрузка, успех, ошибка, пусто), которые компонент отображает во время асинхронных операций; правильное управление ими — обязательная часть UX.
Зачем нужно
Без loading states пользователь видит пустой экран или устаревшие данные пока запрос выполняется. Правильно спроектированные loading states (скелетоны, спиннеры, индикаторы прогресса) делают приложение отзывчивым и понятным. Типичная ошибка начинающих — забыть состояние ошибки, и пользователь видит пустой список без объяснений.
Где используется
- Любой компонент с асинхронной загрузкой данных (списки, детали, дашборды)
- Формы: состояние отправки (
isSubmitting) и успеха/ошибки - Infinite scroll и пагинация
- Загрузка файлов с прогрессом
Паттерн: состояния загрузки
function UserList() {
const [state, setState] = useState({
data: null,
isLoading: true,
error: null,
});
useEffect(() => {
setState({ data: null, isLoading: true, error: null });
fetch('/api/users')
.then(r => {
if (!r.ok) throw new Error(`HTTP ${r.status}`);
return r.json();
})
.then(data => setState({ data, isLoading: false, error: null }))
.catch(error => setState({ data: null, isLoading: false, error }));
}, );
// Явная обработка каждого состояния
if (state.isLoading) {
return <UserListSkeleton />; // скелетон лучше спиннера
}
if (state.error) {
return (
<ErrorMessage
message={`Не удалось загрузить пользователей: ${state.error.message}`}
onRetry={ => setState(prev => ({ ...prev, isLoading: true }))}
/>
);
}
if (!state.data || state.data.length === 0) {
return <EmptyState message="Пользователей не найдено" />;
}
return (
<ul>
{state.data.map(user => (
<li key={user.id}>{user.name}</li>
))}
</ul>
);
}
Кастомный хук для загрузки данных
function useFetch(url) {
const [state, setState] = useState({
data: null,
isLoading: true,
error: null,
});
useEffect(() => {
const controller = new AbortController();
setState({ data: null, isLoading: true, error: null });
fetch(url, { signal: controller.signal })
.then(r => r.json())
.then(data => setState({ data, isLoading: false, error: null }))
.catch(err => {
if (err.name !== 'AbortError') {
setState({ data: null, isLoading: false, error: err });
}
});
return => controller.abort();
}, [url]);
return state;
}
// Использование
function ProductPage({ id }) {
const { data: product, isLoading, error } = useFetch(`/api/products/${id}`);
if (isLoading) return <Skeleton />;
if (error) return <ErrorMessage error={error} />;
return <ProductDetails product={product} />;
}
Скелетон вместо спиннера
// Скелетон — лучший UX: показывает структуру страницы
function UserCardSkeleton() {
return (
<div className="skeleton-card">
<div className="skeleton-avatar" />
<div className="skeleton-line skeleton-line--wide" />
<div className="skeleton-line skeleton-line--medium" />
</div>
);
}
function UserListSkeleton() {
return (
<ul>
{Array.from({ length: 5 }).map((_, i) => (
<li key={i}><UserCardSkeleton /></li>
))}
</ul>
);
}
Частые ошибки
- Только два состояния (loading/ready) — забывают об ошибках и пустых данных; пользователь видит пустой список без объяснений.
- Не сбрасывают isLoading перед новым запросом — при смене пропсов (url) показывают старые данные пока грузятся новые.
- Нет AbortController — при быстрой навигации старый запрос возвращается позже нового и перезаписывает актуальные данные (race condition).
- Спиннер для долгих операций — скелетоны снижают воспринимаемое время ожидания лучше, чем вращающийся индикатор.
Связанные темы
- _MOC SPA
- Работа с API в SPA
- Жизненный цикл компонента
- Оптимистичное обновление UI
- Кеширование данных на клиенте