Укрощение Данных: Выбор Стратегии Кэширования в React с React Query vs. SWR

Кэширование данных на клиенте перестало быть опциональным. Медленные интерфейсы, избыточные сетевые запросы и неконтролируемые состояния данных съедают юзабилити и заставляют пользователей уходить. Решение? Библиотеки для управления данными. За последние годы React Query и SWR стали фаворитами в экосистеме React. Но когда выбирать одну вместо другой? Разберем на металле.

Проблема грубого кэширования

Представим типичный компонент:

jsx
function UserProfile({ userId }) {  
  const [user, setUser] = useState(null);  
  const [loading, setLoading] = useState(true);  

  useEffect(() => {  
    fetch(`/api/users/${userId}`)  
      .then(res => res.json())  
      .then(data => setUser(data))  
      .finally(() => setLoading(false));  
  }, [userId]);  

  // Рендер...  
}  

Проблемы:

  • Дублирование запросов: При монтировании двух UserProfile с одним userId — два идентичных запроса.
  • Бесконтрольное устаревание: При изменении данных на сервере клиент не узнает.
  • Отсутствие стратегий инвалидации: Как обновлять данные после мутаций?
  • Нет пагинации/префетчинга.

Ручной контроль состояния быстро превращается в лавину эффектов и рефов. Нам нужны абстракции с четкой моделью данных.

Принципы современного кэширования

Любая библиотека кэширования реализует четыре ключевых паттерна:

  1. Устранение дублей: Запросы с идентичным ключом выполняются однажды.
  2. Фоновое обновление: Устаревшие данные показываются сразу, а параллельно идет silent-запрос за свежими.
  3. Инвалидация: Пометить данные как устаревшие, чтобы запросить актуальные.
  4. Сборка мусора: Удалять неиспользуемые данные из кэша.

React Query и SWR реализуют эти паттерны, но с разным подходом.

React Query: Тяжелая артиллерия

React Query (RQ) построен на "QueryClient" — централизованном хранилище данных. Базовая выборка:

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

async function fetchUser(id) {  
  const res = await fetch(`/api/users/${id}`);  
  if (!res.ok) throw new Error('Network error');  
  return res.json();  
}  

function UserProfile({ userId }) {  
  const {  
    data: user,  
    isLoading,  
    isError  
  } = useQuery({  
    queryKey: ['users', userId],  // Уникальный ключ  
    queryFn: () => fetchUser(userId),  
    staleTime: 5 * 60 * 1000, // 5 минут до устаревания  
  });  

  // Рендер...  
}  

Особенности реализации:

  • Query Keys как dependency array: Ключи (['users', userId]) — массив любого сериализуемого типа. RQ отслеживает их изменения для перезапроса.

  • Инвалидация:

    js
    // Где-то в обработчике мутации  
    await updateUserName(userId, newName);  
    queryClient.invalidateQueries(['users', userId]);  
    

    RQ автоматически повторяет запросы с таким ключом.

  • Жизненный цикл запроса:

    • freshstaleinactive → garbage collected.
    • Данные удаляются после 5 минут без подписчиков (настраивается).
  • Оптимистичные обновления:
    RQ сохраняет снапшот состояния перед мутацией для мгновенного отката.

RQ — это полноценный менеджер состояния асинхронных данных с поддержкой дебага через DevTools.

SWR: Минимализм и скорость

SWR (Stale-While-Revalidate) фокусируется на сценариях чтения данных. Синтаксис:

jsx
import useSWR from 'swr';  

const fetcher = (...args) => fetch(...args).then(res => res.json());  

function UserProfile({ userId }) {  
  const {  
    data: user,  
    error,  
    isLoading  
  } = useSWR(`/api/users/${userId}`, fetcher, {  
    revalidateIfStale: true, // Автообновление устаревших данных  
    dedupingInterval: 2000   // Интервал дедупликации (мс)  
  });  

  // Рендер...  
}  

Архитектурные нюансы:

  • Автоматическая дедупликация: Все хуки с одним ключом (URL) получают один экземпляр запроса.
  • Focus Revalidation: При возврате на вкладку SWR автоматически обновляет данные.
  • Зависимые запросы:
    js
    // Получаем user, затем его заказы  
    const { data: user } = useSWR(`/api/users/${userId}`);  
    const { data: orders } = useSWR(  
      user ? `/api/users/${userId}/orders` : null, // Ключ-null = запрос не запускается  
      fetcher  
    );  
    
  • Мутация через mutate:
    js
    const { mutate } = useSWRConfig();  
    
    mutate(`/api/users/${userId}`, newUserData, {  
      optimisticData: newUserData, // Мгновенное обновление  
      revalidate: true             // Фоновое обновление с сервера  
    });  
    

SWR не имеет встроенной сборки мусора, но это компенсируется малой площадью API (5-6 кб).

Сравнение: Что Когда Выбрать

RQ выигрывает если:

  • Приложение взаимодействует с разными типами API (REST + GraphQL через адаптеры);
  • Нужны сложные зависимости между запросами (запрос B запускается после успеха A);
  • Требуются тонкие правила инвалидации (например, инвалидировать все ключи с префиксом ['posts']);
  • Вам критично время жизни кэша и сборка мусора.

SWR предпочтителен если:

  • Приложение работает в основном с RESTful API;
  • Приоритет — скорость внедрения и минимализм кода;
  • Нужны ленивые зависимые запросы через ключ-функцию;
  • Бюджет на размер бандла жестко ограничен.

Производительность:

  • RQ: Ресурсоемкие приложения (>100 активных подписок) требуют настройки garbage collection.
  • SWR: Изящен на старте, но возможны лишние ререндеры при частом использовании mutate.

Продвинутые паттерны

Предзагрузка данных:
RQ:

js
// В event handler или useEffect  
queryClient.prefetchQuery({  
  queryKey: ['user', userId],  
  queryFn: fetchUser  
});  

SWR:

js
import { mutate } from 'swr';  
mutate(`/api/users/${userId}`, fetch(`/api/users/${userId}`).then(r => r.json()));  

Автоматические повторные запросы при ошибках сети (retry):
Обе библиотеки настраивают политику через retry и retryDelay.

Бесконечная загрузка:
RQ:

jsx
const { fetchNextPage, hasNextPage } = useInfiniteQuery({  
  queryKey: ['posts'],  
  queryFn: ({ pageParam = 0 }) => fetchPosts(pageParam),  
  getNextPageParam: (lastPage) => lastPage.nextCursor  
});  

SWR:

js
const { data, setSize } = useSWRInfinite(  
  (pageIndex) => `/api/posts?page=${pageIndex}`,  
  fetcher  
);  

Заключение

  • React Query предлагает всеобъемлющий контроль для крупных приложений с интенсивной работой с данными. Его сила — в предсказуемой модели жизненного цикла.
  • SWR — элегантный инструмент для быстрого прототипирования и приложений с фокусом на чтении данных.

Главное — не выбор «лучшей» библиотеки, а определение требований к данным в вашем приложении. Обеим библиотекам чужда магия: вы всегда контролируете состояние и поведение. Начните с SWR для скорости и минимализма. Если проекту потребуются расширенные функции мутаций и инвалидации — React Query прикроет тыл. И всегда помните: кэширование — не замена архитектуре API, а умный буфер между пользователем и сервером.

Примечание: Примеры используют умолчания библиотек. Всегда сверяйтесь с официальной документацией (React Query v5, SWR 2.x), где детали API могут уточняться.