Введение
Представьте, что вы нажали кнопку для получения данных, и ничего не произошло. Экран замер, и вы начнете паниковать: сломался интернет? или сервер упал? или приложение вообще не работает? Теперь представьте вместо этого кружок «Загрузка…». У вас сразу появится уверенность, что процесс идет, и вы просто ждете.
Итак, управление состоянием загрузки:
- Создает предсказуемость интерфейса.
- Улучшает пользовательский опыт (UX).
- Дает обратную связь о состоянии приложения.
- Помогает отлавливать ошибки при долгих задержках.
Создаем индикатор загрузки шаг за шагом
1. Управление состоянием с useState
Для отображения индикатора загрузки нам нужно знать, происходит ли запрос в данный момент. Для этого мы используем штатный React-хук useState.
Вот базовый пример обработки состояния загрузки с fetch:
import React, { useState } from 'react';
const FetchWithLoading: React.FC = () => {
const [isLoading, setIsLoading] = useState(false);
const [data, setData] = useState<null | string>(null);
const fetchData = async () => {
setIsLoading(true); // Включаем индикатор загрузки
try {
const response = await fetch('https://jsonplaceholder.typicode.com/posts/1');
const result = await response.json();
setData(result.title); // Сохраняем данные
} catch (error) {
console.error('Ошибка загрузки:', error);
} finally {
setIsLoading(false); // Выключаем индикатор загрузки
}
};
return (
<div>
<button onClick={fetchData}>Загрузить данные</button>
{isLoading && <p>Загрузка...</p>}
{data && <p>Результат: {data}</p>}
</div>
);
};
export default FetchWithLoading;
Разберем код:
- Состояние
isLoading:- Управляет отображением индикатора загрузки:
true, пока идёт запрос. - Устанавливаем
trueперед началом запроса и сбрасываем вfalseпосле завершения (вfinally).
- Управляет отображением индикатора загрузки:
- Индикатор
Загрузка...:- Условный рендеринг: отображаем текст, если
isLoading === true.
- Условный рендеринг: отображаем текст, если
2. Разделяем логику и UI через кастомные хуки
Давайте вынесем логику загрузки в кастомный хук, чтобы избежать повторения кода в разных местах:
import { useState } from 'react';
export const useFetchWithLoading = (url: string) => {
const [isLoading, setIsLoading] = useState(false);
const [data, setData] = useState<null | any>(null);
const [error, setError] = useState<string | null>(null);
const fetchData = async () => {
setIsLoading(true);
setError(null); // Сбрасываем предыдущую ошибку
try {
const response = await fetch(url);
if (!response.ok) {
throw new Error(`Ошибка HTTP: ${response.status}`);
}
const result = await response.json();
setData(result);
} catch (err) {
setError((err as Error).message);
} finally {
setIsLoading(false);
}
};
return { isLoading, data, error, fetchData };
};
Теперь используем наш хук в компоненте:
import React from 'react';
import { useFetchWithLoading } from './useFetchWithLoading';
const DataWithCustomHook: React.FC = () => {
const { isLoading, data, error, fetchData } = useFetchWithLoading(
'https://jsonplaceholder.typicode.com/posts/1'
);
return (
<div>
<button onClick={fetchData}>Загрузить данные</button>
{isLoading && <p>Загрузка...</p>}
{error && <p style={{ color: 'red' }}>Ошибка: {error}</p>}
{data && <p>Результат: {data.title}</p>}
</div>
);
};
export default DataWithCustomHook;
Теперь, если мы захотим подключить другую API, нам достаточно передать новый url в хук.
Добавляем "визуальную магию"
Серое "Загрузка..." — это скучно. Давайте сделаем что-то более визуально привлекательное. Например, используем спиннер.
Используем CSS-анимацию
/* spinner.css */
.spinner {
border: 4px solid rgba(0, 0, 0, 0.1);
width: 40px;
height: 40px;
border-radius: 50%;
border-left-color: #09f;
animation: spin 1s linear infinite;
}
@keyframes spin {
to {
transform: rotate(360deg);
}
}
Теперь добавим этот спиннер в наш компонент:
import React from 'react';
import './spinner.css';
import { useFetchWithLoading } from './useFetchWithLoading';
const FancyLoader: React.FC = () => {
const { isLoading, data, error, fetchData } = useFetchWithLoading(
'https://jsonplaceholder.typicode.com/posts/1'
);
return (
<div>
<button onClick={fetchData}>Загрузить данные</button>
{isLoading && <div className="spinner"></div>}
{error && <p style={{ color: 'red' }}>Ошибка: {error}</p>}
{data && <p>Результат: {data.title}</p>}
</div>
);
};
export default FancyLoader;
Теперь вместо текста "Загрузка..." у нас будет стильный вращающийся спиннер.
Отображение прогресса загрузки (если доступно)
Для загрузки больших файлов можно показывать прогресс. Давайте попробуем это реализовать:
const fetchWithProgress = async (url: string, onProgress: (progress: number) => void) => {
const response = await fetch(url);
const reader = response.body?.getReader();
const contentLength = Number(response.headers.get('Content-Length'));
let receivedLength = 0;
const chunks: Uint8Array[] = [];
while (true) {
const { done, value } = await reader!.read();
if (done) break;
chunks.push(value!);
receivedLength += value!.length;
onProgress((receivedLength / contentLength) * 100);
}
const blob = new Blob(chunks);
return blob.text();
};
Используем функцию в компоненте:
import React, { useState } from 'react';
const ProgressLoader: React.FC = () => {
const [progress, setProgress] = useState(0);
const loadData = async () => {
const data = await fetchWithProgress('https://example.com/large-file', setProgress);
console.log(data);
};
return (
<div>
<button onClick={loadData}>Загрузить данные</button>
<p>Прогресс загрузки: {progress.toFixed(2)}%</p>
</div>
);
};
export default ProgressLoader;
Подводные камни и типичные ошибки
- Индикатор исчезает слишком рано или слишком поздно? Убедитесь, что состояние
isLoadingобновляется в правильный момент. Не забывайте использоватьfinallyдля сброса состояния в любом исходе. - Визуальная перегруженность: не показывайте одновременно индикатор загрузки и результат. Всегда уделяйте внимание UX.
- Огромное количество запросов: если вы используете индикатор в списках (например, при инфинит-скролле), объедините их состояние в массивы, чтобы избежать хаоса.
Теперь ваш пользовательский интерфейс будет не только функциональным, но и отзывчивым!
ПЕРЕЙДИТЕ В ПОЛНУЮ ВЕРСИЮ