Как организовать работу с асинхронными запросами в React-приложении?
Организация работы с асинхронными запросами в React-приложении — это важный аспект современного фронтенд-разработки. От правильного подхода зависит стабильность, масштабируемость и производительность интерфейса. Ниже подробно разобраны основные способы работы с асинхронностью в React, включая ручной подход, использование useEffect, современные абстракции и специализированные библиотеки.
1. Асинхронные запросы вручную с fetch + useEffect
Самый базовый подход: использовать встроенную функцию fetch, оборачивая запрос в useEffect и отслеживая состояния загрузки.
Пример:
import { useState, useEffect } from 'react';
function UsersList() {
const \[users, setUsers\] = useState(\[\]);
const \[loading, setLoading\] = useState(true);
const \[error, setError\] = useState(null);
useEffect(() => {
let isMounted = true;
fetch('https://jsonplaceholder.typicode.com/users')
.then(res => res.json())
.then(data => {
if (isMounted) {
setUsers(data);
setLoading(false);
}
})
.catch(err => {
if (isMounted) {
setError(err);
setLoading(false);
}
});
return () => {
isMounted = false;
};
}, \[\]);
if (loading) return <p>Loading...</p>;
if (error) return <p>Error!</p>;
return (
<ul>
{users.map(user => <li key={user.id}>{user.name}</li>)}
</ul>
);
}
Плюсы:
-
Полный контроль над загрузкой.
-
Без сторонних зависимостей.
Минусы:
-
Много шаблонного кода: загрузка, ошибки, очистка.
-
Сложнее обрабатывать повторный fetch, кеширование, отмену запроса.
2. Асинхронность с async/await в useEffect
React useEffect не может быть async напрямую, но внутри него можно использовать async функцию.
useEffect(() => {
const fetchData = async () => {
try {
const res = await fetch('/api/data');
const data = await res.json();
setData(data);
} catch (e) {
setError(e);
}
};
fetchData();
}, \[\]);
Это улучшает читаемость по сравнению с .then().catch().
3. Обработка отмены запроса (AbortController)
При размонтировании компонента запрос может быть ещё в процессе. Чтобы избежать ошибок, можно отменить запрос.
useEffect(() => {
const controller = new AbortController();
fetch('/api/data', { signal: controller.signal })
.then(res => res.json())
.then(data => setData(data))
.catch(e => {
if (e.name !== 'AbortError') setError(e);
});
return () => controller.abort();
}, \[\]);
4. Использование кастомных хуков (useFetch, useApi)
Хорошая практика — выносить логику загрузки данных в переиспользуемые хуки.
function useUsers() {
const \[data, setData\] = useState(null);
const \[loading, setLoading\] = useState(true);
const \[error, setError\] = useState(null);
useEffect(() => {
fetch('/api/users')
.then(res => res.json())
.then(setData)
.catch(setError)
.finally(() => setLoading(false));
}, \[\]);
return { data, loading, error };
}
Это позволяет сделать компонент «тупым» и облегчить тестирование.
5. axios как альтернатива fetch
axios — популярная обёртка над XMLHttpRequest, предлагающая более удобный API и автоматическую трансформацию JSON.
Пример:
axios.get('/api/data')
.then(response => setData(response.data))
.catch(setError);
Также позволяет настроить interceptors для авторизации, обработки ошибок и логирования.
6. React Query (ныне TanStack Query)
Библиотека для загрузки, кеширования и управления асинхронными данными. Позволяет избавиться от ручной логики загрузки, обработки ошибок и обновления кэша.
Пример:
import { useQuery } from '@tanstack/react-query';
function Users() {
const { data, isLoading, isError } = useQuery({
queryKey: \['users'\],
queryFn: () => fetch('/api/users').then(res => res.json()),
});
if (isLoading) return 'Loading...';
if (isError) return 'Error!';
return <ul>{data.map(u => <li key={u.id}>{u.name}</li>)}</ul>;
}
Возможности:
-
Кеширование запросов.
-
Инвалидация данных.
-
Пагинация, бесконечная прокрутка.
-
Повторные попытки при ошибке.
-
Отложенные и фоновые запросы.
-
Работа с асинхронностью и SSR.
Плюсы:
-
Много логики — из коробки.
-
Сильно сокращает шаблонный код.
-
Оптимизация ререндеров.
Минусы:
-
Нужно привыкнуть к новой парадигме.
-
Хранит данные в кэше, а не в глобальном состоянии.
7. Состояние загрузки и асинхронность с Suspense
React поддерживает отложенную загрузку компонентов с помощью Suspense. С помощью новых подходов (например, React Server Components и асинхронных хуков) можно загружать данные на уровне компонента.
Пример:
<Suspense fallback={<Loading />}>
<UserProfile />
</Suspense>
В будущем:
-
Асинхронные хуки (use) на клиенте появятся в стабильном виде.
-
Интеграция с React Server Components (Next.js 14+).
8. Управление асинхронными запросами через Zustand, Redux и др.
Redux:
Используют middleware для работы с асинхронными действиями:
-
redux-thunk
-
redux-saga
-
redux-observable
-
RTK Query (встроенный в Redux Toolkit)
Пример с RTK Query:
const usersApi = createApi({
reducerPath: 'usersApi',
baseQuery: fetchBaseQuery({ baseUrl: '/api' }),
endpoints: builder => ({
getUsers: builder.query({
query: () => 'users'
})
})
});
Zustand:
const useStore = create((set) => ({
users: \[\],
fetchUsers: async () => {
const res = await fetch('/api/users');
const data = await res.json();
set({ users: data });
}
}));
9. Обработка ошибок и состояния загрузки
Рекомендуется явно обрабатывать:
-
loading — пока идёт загрузка данных.
-
error — при ошибке соединения или получения данных.
-
empty — когда данные получены, но список пуст.
Это повышает UX и надёжность приложения.
10. Повторные и зависимые запросы
Иногда нужно:
-
Загружать вторичные данные после первых.
-
Повторно запрашивать при изменении параметров.
Пример с useEffect:
useEffect(() => {
if (!userId) return;
fetch(\`/api/users/${userId}\`).then(...);
}, \[userId\]);
Пример с React Query:
useQuery(\['user', userId\], () => fetchUser(userId), { enabled: !!userId });
enabled блокирует выполнение до появления userId.
Каждая стратегия имеет свои сферы применения. Простые fetch + useEffect хороши для небольших задач. Для крупных приложений с API — стоит использовать React Query или RTK Query. Это избавляет от ручного управления состоянием, ошибок и кеширования.