Оптимизация данных в React: Глубокое погружение в React Query

Исходная проблема: много данных, масса проблем
Ваше React-приложение выросло. Запросы накапливаются как снежный ком. Где хранить данные — в Redux, Context, локальном состоянии? Как синхронизировать с сервером? Кэширование, инвалидация, фоновое обновление... Всё это превращает разработку в головную боль. Неоптимальные решения приводят к мерцанию интерфейсов, лагам и раздутому коду.

Почему классические подходы терпят неудачу

  • Redux/Context overkill: Ручное управление загрузкой, ошибками, обновлением данных превращает редьюсеры в монстров.
  • Избыточные запросы: Компоненты независимо дергают одни и те же данные при каждом рендере.
  • Несогласованность: Данные в UI расходятся с серверным состоянием после мутаций.
  • Пагинация/бесконечная лента: Реализация "с нуля" требует тонкой ручной настройки.

React Query не просто кэш — это машина состояния данных:
Эта библиотека абстрагирует серверное состояние, оставляя в UI только то, что действительно нужно компонентам.

jsx
import { useQuery } from '@tanstack/react-query';

const fetchUser = async (id) => {  
  const res = await fetch(`/api/users/${id}`);  
  if (!res.ok) throw new Error('Ошибка загрузки');  
  return res.json();  
};  

function UserProfile({ userId }) {  
  const {  
    data: user,  
    isLoading,  
    error,  
    isFetching  
  } = useQuery({  
    queryKey: ['user', userId],  
    queryFn: () => fetchUser(userId),  
    staleTime: 5 * 60 * 1000, // 5 минут до "устаревания"  
  });  

  if (isLoading) return <Spinner />;  
  if (error) return <Error message={error.message} />;  

  return (  
    <div>  
      <h1>{user.name}</h1>  
      {isFetching && <small>Обновление...</small>}  
    </div>  
  );  
}  

Всего 10 строк, но получили:

  • Кэширование между компонентами
  • Фоновое обновление при повторном входе в компонент
  • Индикатор фоновой загрузки
  • Переиспользование одинаковых запросов

Ключевые паттерны, которые стоит взять на вооружение

Инвалидация запросов после мутаций
После изменения данных на сервере (POST/PUT/DELETE) кэш нужно помечать устаревшим. Решение: queryClient.invalidateQueries.

jsx
const queryClient = useQueryClient();  

const { mutate } = useMutation({  
  mutationFn: updateUser,  
  onSuccess: () => {  
    // Принудительное обновление всех запросов по ключу 'user'  
    queryClient.invalidateQueries({ queryKey: ['user'] });  
  }  
});  

Оптимистичные обновления
Не ждите ответа сервера — применяйте UI-изменения сразу, отменяя при ошибке:

jsx
useMutation({  
  mutationFn: updateUser,  
  onMutate: async (newUser) => {  
    await queryClient.cancelQueries({ queryKey: ['user', newUser.id] });  
    const prevUser = queryClient.getQueryData(['user', newUser.id]);  
    queryClient.setQueryData(['user', newUser.id], newUser); // Оптимистичный апдейт  
    return { prevUser }; // Для отката  
  },  
  onError: (err, newUser, context) => {  
    queryClient.setQueryData(['user', newUser.id], context.prevUser);  
  }  
});  

Префетчинг данных
Заранее подгружайте данные при наведении на кнопку или перед навигацией:

jsx
const queryClient = useQueryClient();  

<Link  
  to={`/users/${id}`}  
  onMouseEnter={() =>  
    queryClient.prefetchQuery({  
      queryKey: ['user', id],  
      queryFn: () => fetchUser(id),  
    })  
  }  
>  
  Профиль  
</Link>  

Подводные камни и как их избежать

Ключи запросов — ваша новая главная абстракция

  • Нормализуйте их структуру: ['resource', id, 'sub-resource', { params }].
  • Избегайте динамичных ключей без ограничений (['posts', Date.now()] — антипаттерн).

Стаблити и рифети: настройте тонко

  • staleTime: Укажите, когда данные считаются устаревшими (по умолчанию 0).
  • refetchInterval: Автоперезапрос для live-данных (чаты, курсы).
  • retry, retryDelay: Стратегия обработки ошибок.

Когда НЕ использовать React Query?

  • Работа с чисто клиентским состоянием (чекбоксы, модалки).
  • Действия без асинхронной логики (фильтрация массива).

Глубже в архитектуру

Интеграция с клиент-стэйтеми (Zustand/Jotai):

jsx
// Zustand + React Query = Синхронное состояние сервера/клиента  
const useUserStore = create((set) => ({  
  user: null,  
  fetchUser: async (id) => {  
    const data = await queryClient.fetchQuery(['user', id]);  
    set({ user: data });  
  }  
}));  

Ошибка "No QueryClient set":
Оберните корневой компонент в <QueryClientProvider client={queryClient}>.

Пагинация за 5 минут:

jsx
const { data, fetchNextPage, hasNextPage } = useInfiniteQuery({  
  queryKey: ['projects'],  
  queryFn: ({ pageParam = 1 }) => fetchProjects(pageParam),  
  getNextPageParam: (lastPage) => lastPage.nextPage,  
});  

Философия React Query

Перестаньте считать данные "своими". Сервер — единственный источник истины. React Query выступает посредником: он знает, когда данные устарели, когда их нужно обновить, как минимизировать нагрузку.

Итоговый чеклист для интеграции:

  1. Определите уникальные ключи для всех ресурсов.
  2. Настройте глобальный queryClient (флаг defaultOptions).
  3. Замените useState/useEffect запросов на useQuery.
  4. Оптимизируйте: префетчинг при ховере, стабл-тайм 2+ минуты.
  5. Инвалидируйте кэш после мутаций.

Вы освобождаете 30% кода от рутины. Данные становятся согласованными. Интерфейс реагирует мгновенно. Браузер меньше дергает сеть. Сервер получает меньше дублирующих запросов. Это не библиотека — это смена парадигмы управления состоянием.

Когда вы переосмыслите поток данных в приложении, окажется: большая часть проблем имеют готовые стандартные решения. Ваша задача — перестать их писать вручную и довериться инструментам.