Кеширование данных на клиенте

Клиентское кеширование данных — хранение результатов API-запросов в памяти приложения для повторного использования без повторного сетевого запроса.

Зачем нужно

В SPA пользователь часто возвращается к уже просмотренным данным: открыл список товаров, перешёл в карточку, вернулся. Без кеша каждый переход — новый fetch-запрос и мерцание загрузчика. С кешем возврат к списку мгновенный, а в фоне React Query тихо проверяет актуальность данных. Это радикально улучшает UX и снижает нагрузку на API.

Где используется

  • React Query (TanStack Query) — стандарт для server state в React
  • SWR (stale-while-revalidate) — от Vercel, аналог React Query
  • Apollo Client — GraphQL-запросы с нормализованным кешем
  • localStorage / sessionStorage — долгосрочное хранение токенов, настроек
  • Service Workers — оффлайн-кеш на уровне браузера

React Query: кеш по умолчанию

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

const queryClient = new QueryClient({
  defaultOptions: {
    queries: {
      staleTime: 5 * 60 * 1000,  // данные свежие 5 минут — нет refetch
      gcTime: 10 * 60 * 1000,    // кеш хранится 10 минут после последнего использования
    },
  },
});

function App() {
  return (
    <QueryClientProvider client={queryClient}>
      <ProductsPage />
    </QueryClientProvider>
  );
}

// При навигации назад к этой странице — данные берутся из кеша мгновенно
// В фоне React Query проверяет актуальность (если staleTime истёк)
function ProductsPage() {
  const { data: products, isLoading, isFetching } = useQuery({
    queryKey: ['products'],        // ключ кеша
    queryFn:  => fetchProducts,
    staleTime: 60_000,             // 1 минута
  });

  return (
    <div>
      {/* isFetching: true когда идёт фоновое обновление */}
      {isFetching && <span className="refresh-indicator">Обновление...</span>}
      {isLoading ? <Skeleton /> : <ProductList products={products} />}
    </div>
  );
}

// Данные по конкретному продукту кешируются отдельно
function ProductDetail({ id }) {
  const { data: product } = useQuery({
    queryKey: ['products', id],   // уникальный ключ для каждого id
    queryFn:  => fetchProduct(id),
  });

  return product ? <div>{product.name}</div> : <Skeleton />;
}

Инвалидация кеша после мутации

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

function CreateProductForm() {
  const queryClient = useQueryClient;

  const mutation = useMutation({
    mutationFn: (newProduct) => createProduct(newProduct),
    onSuccess:  => {
      // Инвалидируем кеш списка — следующий рендер ProductsPage сделает новый запрос
      queryClient.invalidateQueries({ queryKey: ['products'] });
    },
  });

  return (
    <form onSubmit={(e) => {
      e.preventDefault();
      mutation.mutate({ name: 'New Product', price: 999 });
    }}>
      <button disabled={mutation.isPending}>
        {mutation.isPending ? 'Создание...' : 'Создать'}
      </button>
    </form>
  );
}

Ручной кеш в памяти (простой вариант)

// Простой in-memory кеш без библиотеки
const cache = new Map();

async function cachedFetch(url, ttl = 60_000) {
  const cached = cache.get(url);

  if (cached && Date.now() - cached.timestamp < ttl) {
    return cached.data; // возвращаем из кеша
  }

  const data = await fetch(url).then(r => r.json());
  cache.set(url, { data, timestamp: Date.now() });
  return data;
}

Частые ошибки

  • Кеш без инвалидации — после создания/обновления/удаления данных кеш содержит устаревшее состояние; вызывайте invalidateQueries.
  • Слишком длинный staleTime — данные остаются «свежими» и не обновляются даже когда изменились на сервере.
  • Кеширование персонализированных данных без учёта пользователя — ключ кеша должен включать userId если данные зависят от пользователя: ['user-products', userId].

Связанные темы

Ресурсы