Кэширование API с помощью React Query: кейс из реального проекта

Автор: Javohir Abdullayev · · React и Frontend

Кэширование API с помощью React Query: кейс из реального проекта

Введение: Головная боль с API-запросами в крупном проекте

Каждый фронтенд-разработчик хотя бы раз в своей практике сталкивался со следующей проблемой: при переходе пользователя между страницами одни и те же данные повторно загружаются с сервера, количество спиннеров загрузки растет, а нагрузка на сервер увеличивается. В этой статье мы рассмотрим, как решили подобную проблему в реальном проекте панели управления электронной коммерцией (admin dashboard) с помощью TanStack Query (React Query).

Контекст проекта и первоначальная проблема

В разработанной нами CRM-системе были список клиентов, статистика заказов и каталог товаров. Все данные загружались классическим способом через useEffect и useState:

// Устаревший ошибочный подход
const [products, setProducts] = useState([]);
const [loading, setLoading] = useState(true);

useEffect(() => {
  fetch('/api/products')
    .then(res => res.json())
    .then(data => {
      setProducts(data);
      setLoading(false);
    });
}, []);

Сначала казалось, что этот подход работает нормально, однако по мере роста проекта возникли серьезные проблемы:

  • Ненужные повторные запросы (Duplicate requests): Каждый раз, когда пользователь перемещался между страницами (например, Товары → Настройки → Товары), весь список запрашивался заново, вызывая мерцание экрана (flicker).
  • Устаревшие данные (Stale data): При одновременной работе нескольких администраторов изменение цены одним админом не обновлялось на экране другого.
  • Утечки памяти и неотмененные запросы: При размонтировании компонента запрос продолжал выполняться, вызывая предупреждения в консоли.
  • Путаница в глобальном состоянии: Использование Redux только для хранения данных с сервера перегружало кодовую базу лишним бойлерплейтом.

Решение: Интеграция TanStack Query

Для решения проблемы мы перешли на библиотеку @tanstack/react-query. Она берет на себя управление серверным состоянием (server state), кэширование, синхронизацию и фоновое обновление данных в автоматическом режиме.

1. Настройка QueryClient

Сначала в основном файле проекта мы правильно настроили время жизни кэша (staleTime и gcTime):

import { QueryClient, QueryClientProvider } from '@tanstack/react-query';

const queryClient = new QueryClient({
  defaultOptions: {
    queries: {
      staleTime: 1000 * 60 * 5, // Данные считаются свежими в течение 5 минут
      gcTime: 1000 * 60 * 10,   // Хранятся в кэше 10 минут
      refetchOnWindowFocus: false,
      retry: 1,
    },
  },
});

export default function App() {
  return (
    <QueryClientProvider client={queryClient}>
      <DashboardLayout />
    </QueryClientProvider>
  );
}

2. Создание кастомного хука (Custom Hook)

Чтобы сделать вызовы API чистыми и переиспользуемыми, мы написали специальный хук:

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

const fetchProducts = async (page = 1) => {
  const { data } = await axios.get(`/api/products?page=${page}`);
  return data;
};

export const useProducts = (page) => {
  return useQuery({
    queryKey: ['products', page],
    queryFn: () => fetchProducts(page),
    placeholderData: (previousData) => previousData, // плавный переход для пагинации
  });
};

3. Обновление данных и оптимистичные обновления (Optimistic Updates)

Мгновенное отображение изменений в интерфейсе (Optimistic Update) при редактировании цены товара пользователем, не дожидаясь ответа сервера, существенно улучшило пользовательский опыт (UX):

import { useMutation, useQueryClient } from '@tanstack/react-query';
import axios from 'axios';

export const useUpdateProduct = () => {
  const queryClient = useQueryClient();

  return useMutation({
    mutationFn: (updatedProduct) => axios.put(`/api/products/${updatedProduct.id}`, updatedProduct),
    onMutate: async (newProduct) => {
      await queryClient.cancelQueries({ queryKey: ['products'] });
      const previousProducts = queryClient.getQueryData(['products']);
      
      queryClient.setQueryData(['products'], (old) => 
        old?.map(item => item.id === newProduct.id ? { ...item, ...newProduct } : item)
      );
      
      return { previousProducts };
    },
    onError: (err, newProduct, context) => {
      queryClient.setQueryData(['products'], context.previousProducts);
    },
    onSettled: () => {
      queryClient.invalidateQueries({ queryKey: ['products'] });
    },
  });
};

Результаты: Какие изменения произошли в проекте?

После полного перехода на React Query мы достигли следующих конкретных показателей в нашем проекте:

  1. Нагрузка на сервер снизилась на 65%: Благодаря правильной настройке staleTime лишние повторные запросы от одного и того же пользователя практически полностью исчезли.
  2. Объем Redux сократился на 70%: В Redux осталось только чисто клиентское состояние (например, состояние сайдбара, выбор темы). Все состояния API были переданы под управление React Query.
  3. Улучшилась скорость приложения и UX: Переходы между страницами стали мгновенными, так как данные читаются напрямую из кэша и обновляются в фоновом режиме.

Заключение и рекомендации

Если в вашем React-проекте много API-запросов и вы устали загружать данные внутри useEffect, обязательно используйте TanStack Query. Он не только сократит ваш код, но и сделает приложение в разы быстрее для пользователей.

Теги: #React #TanStack Query #Frontend #Optimizatsiya #API