# Оптимизация управления состоянием данных в React с помощью React Query
Если вы разрабатываете современные React-приложения, вы наверняка сталкивались с необходимостью получения, кэширования, синхронизации и обновления данных с бэкенда. Традиционные подходы с использованием Redux или контекста часто ведут к сложностям: избыточному коду, ручному управлению кэшем, избыточным рендерингам и сложностям обработки ошибок. Рассмотрим лучший способ.
## Проблема ручного управления данными
Типичная реализация с useEffect выглядит примерно так:
```jsx
function UserProfile({ userId }) {
const [userData, setUserData] = useState(null);
const [loading, setLoading] = useState(true);
const [error, setError] = useState(null);
useEffect(() => {
let isMounted = true;
const fetchData = async () => {
try {
const response = await fetch(`/api/users/${userId}`);
const data = await response.json();
if (isMounted) {
setUserData(data);
setLoading(false);
}
} catch (err) {
if (isMounted) {
setError(err.message);
setLoading(false);
}
}
};
fetchData();
return () => {
isMounted = false;
};
}, [userId]);
if (loading) return <Spinner />;
if (error) return <Error message={error} />;
return (
<div>
<h1>{userData.name}</h1>
<p>{userData.bio}</p>
</div>
);
}
Такой подход содержит несколько известных проблем:
- Необходимость ручного отслеживания состояния загрузки и ошибок
- Проблемы гонки при быстрой смене userId
- Отсутствие встроенного кэширования
- Неявные расчёты времени устаревания данных
- Отсутствие автоматических повторных запросов
- Необходимость вручную отменять запросы при размонтировании
Введение React Query
React Query (теперь TanStack Query) решает эти проблемы с помощью децентрализованного подхода к управлению данных. Основные понятия:
import { QueryClient, QueryClientProvider, useQuery } from 'react-query';
const queryClient = new QueryClient();
function App() {
return (
<QueryClientProvider client={queryClient}>
<UserProfile userId="123" />
</QueryClientProvider>
);
}
function UserProfile({ userId }) {
const { data, isLoading, error } = useQuery(
['user', userId],
() => fetch(`/api/users/${userId}`).then(res => res.json())
);
if (isLoading) return <Spinner />;
if (error) return <Error message={error.message} />;
return (
<div>
<h1>{data.name}</h1>
<p>{data.bio}</p>
</div>
);
}
Всего 5 строк логики вместо 30, но за кажущейся простотой скрывается мощная функциональность.
Автоматическое кэширование и инвалидация
Ключевое преимущество React Query - разумное кэширование. По умолчанию данные кэшируются на 5 минут (staleTime), но лучше настроить это явно:
const queryClient = new QueryClient({
defaultOptions: {
queries: {
staleTime: 60 * 1000 * 10, // 10 минут
cacheTime: 60 * 1000 * 20, // 20 минут
},
},
});
Инвалидация данных:
function UserProfile({ userId }) {
const queryClient = useQueryClient();
const { data } = useQuery(['user', userId], fetchUser);
const updateName = useMutation(
(newName) => patchUser(userId, { name: newName }),
{
onSuccess: () => {
// Инвалидируем конкретный запрос
queryClient.invalidateQueries(['user', userId]);
// Или всех связанных пользовательских запросов
queryClient.invalidateQueries({ predicate: query =>
query.queryKey[0] === 'user'
});
}
}
);
}
Префетчинг данных
React Query упрощает реализацию префетчинга - стратегии, улучшающей UX:
function UserList() {
const queryClient = useQueryClient();
const usersQuery = useQuery('users', fetchUsers);
const handleHover = (userId) => {
queryClient.prefetchQuery(['user', userId], () => fetchUser(userId), {
staleTime: 60 * 1000, // Помечаем как свежие на 60 секунд
});
};
return (
<ul>
{usersQuery.data.map(user => (
<li key={user.id} onMouseEnter={() => handleHover(user.id)}>
{user.name}
</li>
))}
</ul>
);
}
Оптимистичные обновления
Улучшаем восприятие скорости приложения при мутациях:
const updateUser = useMutation(
(newUser) => patchUser(newUser),
{
onMutate: async (newUser) => {
// Отменяем существующие запросы, чтобы избежать перезаписи
await queryClient.cancelQueries(['user', newUser.id]);
// Сохраняем предыдущее значение для отката
const previousUser = queryClient.getQueryData(['user', newUser.id]);
// Устанавливаем новое значение оптимистично
queryClient.setQueryData(['user', newUser.id], newUser);
return { previousUser };
},
onError: (err, newUser, context) => {
// В случае ошибки возвращаем старые данные
queryClient.setQueryData(['user', newUser.id], context.previousUser);
},
onSettled: () => {
// Обновляем данные после успеха или ошибки
queryClient.invalidateQueries(['user', newUser.id]);
}
}
);
Архитектурные соображения
React Query особенно эффективен при правильном разделении логики:
// api.js - централизованное API
const getUsers = async () => {
const response = await fetch('/api/users');
if (!response.ok) throw new Error('Failed to fetch users');
return response.json();
};
const getUserById = async (userId) => {
const response = await fetch(`/api/users/${userId}`);
if (!response.ok) throw new Error(`Failed to fetch user ${userId}`);
return response.json();
};
// hooks.js - кастомные хуки
export const useUsers = () => useQuery('users', getUsers);
export const useUser = (userId) =>
useQuery(['user', userId], () => getUserById(userId));
export const useUpdateUser = () => {
const queryClient = useQueryClient();
return useMutation(
(updatedUser) => patchUser(updatedUser),
{
onSuccess: () => {
queryClient.invalidateQueries('users');
queryClient.invalidateQueries(['user', updatedUser.id]);
}
}
);
};
// UserProfile.jsx - компонент
function UserProfile({ userId }) {
const { data: user } = useUser(userId);
const { mutate: updateUser } = useUpdateUser();
// ... остальная логика компонента
}
Распространенные ошибки и их решения
Слишком широкое кэширование данных
Проблема: Кэширование устаревших данных при изменениях в других частях приложения
Решение:
// При изменении пользователя инвалидируем оба пункта
const useUpdateUser = () => {
const queryClient = useQueryClient();
return useMutation(
updateUserApi,
{
onSuccess: (updatedUser) => {
// Инвалидация точного ключа
queryClient.invalidateQueries(['user', updatedUser.id]);
// Инвалидация групповых запросов
queryClient.invalidateQueries({
queryKey: ['users'],
exact: false,
});
}
}
);
};
Игнорирование фонового обновления
Проблема: Потеря изменений при фоновом обновлении после оптимистичного
Решение:
onMutate: async (newUser) => {
await queryClient.cancelQueries(['user', newUser.id]);
// Сохранение текущих данных
const previousUser = queryClient.getQueryData(['user', newUser.id]);
// Установка новых данных + метка времени оптимистичного обновления
queryClient.setQueryData(['user', newUser.id], {
...newUser,
__timestamp: Date.now(),
});
return { previousUser };
},
onSettled: (data, error, variables, context) => {
// Убеждаемся, что оптимистичное обновление новее фонового
const currentData = queryClient.getQueryData(['user', variables.id]);
if (currentData?.__timestamp < context?.__timestamp) return;
queryClient.invalidateQueries(['user', variables.id]);
}
Некорректная обработка пагинации
Проблема: Неверное слияние данных при пагинации
Решение:
const { data, fetchNextPage } = useInfiniteQuery(
'users',
({ pageParam = 0 }) => fetchUsers({ offset: pageParam, limit: 10 }),
{
getNextPageParam: (lastPage, allPages) =>
lastPage.hasMore ? allPages.length * 10 : undefined,
}
);
// Правильное отображение
return (
<div>
{data.pages.map((page, pageIndex) => (
<React.Fragment key={pageIndex}>
{page.users.map(user => (
<UserCard key={user.id} user={user} />
))}
</React.Fragment>
))}
<button onClick={() => fetchNextPage()}>Загрузить еще</button>
</div>
);
Когда не использовать React Query
Не следует применять этот инструмент для:
- Реактивного UI состояния (лучше useState/useReducer)
- Глобального состояния приложения (лучше Zustand, Recoil)
- Данных, никогда не отправляемых на сервер
- Простых статичных запросов без повторных вызовов
Практические рекомендации
- Уникальность ключей: Используйте продуктогенерирующие структуры для queryKey - массивы с идентификаторами ресурсов
- Разделение ответственности: Создавайте кастомные хуки для каждой конечной точки API
- Обработка ошибок: Централизованный перехват ошибок через
const queryClient = new QueryClient({
queryCache: new QueryCache({
onError: (error) => {
notifyError(error.message);
}
})
});
- Оптимизация рендеров: Для больших списков используйте observerMode
useQuery(queryKey, queryFn, { notifyOnChangeProps: ['data'] });
- Автивность данных: Настройте фонтовое обновление с refetchOnWindowFocus
React Query преобразует архитектуру вашего приложения, позволяя сконцентрироваться на бизнес-логике вместо ручных манипуляций с данными. Это не просто инструмент выборки данных, а комплексное решение для управления состоянием серверных данных, которое особенно ценно в мире микрофронтендов и распределенных систем, где актуальность и синхронизация данных становятся критически важными.