Кеширование данных на клиенте
Клиентское кеширование данных — хранение результатов 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].
Связанные темы
- _MOC SPA
- Загрузка данных и loading states
- Работа с API в SPA
- Оптимистичное обновление UI
- State -- внутреннее состояние