TanStack Query строит работу вокруг концепции серверного состояния. В отличие от локального состояния интерфейса, серверные данные обладают рядом особенностей:
Кеширование в TanStack Query предназначено для решения этих задач автоматически. Библиотека хранит результаты запросов в Query Cache, повторно использует данные между компонентами, контролирует устаревание и управляет повторными запросами.
Каждый запрос в TanStack Query идентифицируется через
queryKey.
useQuery({
queryKey: ['users'],
queryFn: fetchUsers
});
Ключ запроса становится идентификатором записи в кеше.
Структура кеша условно выглядит следующим образом:
{
queries: [
{
queryKey: ['users'],
state: {
data: [...],
status: 'success',
fetchStatus: 'idle',
dataUpdatedAt: 1710000000000
}
}
]
}
Кеш существует внутри экземпляра QueryClient.
const queryClient = new QueryClient();
Все компоненты приложения используют общий клиент через
QueryClientProvider.
После выполнения запроса TanStack Query:
queryFnqueryKeyПример:
const query = useQuery({
queryKey: ['posts'],
queryFn: fetchPosts
});
Если другой компонент вызывает тот же queryKey, сетевой
запрос не выполняется повторно — данные берутся из кеша.
Одной из ключевых концепций является разделение данных на:
По умолчанию данные становятся stale сразу после получения.
useQuery({
queryKey: ['posts'],
queryFn: fetchPosts
});
Это означает:
Параметр staleTime определяет, сколько времени данные
считаются актуальными.
useQuery({
queryKey: ['posts'],
queryFn: fetchPosts,
staleTime: 1000 * 60
});
В течение одной минуты:
staleTime: 0
Подходит для:
Преимущества:
Недостатки:
staleTime: 1000 * 30
Хорошо подходит для:
Позволяет сократить количество запросов без заметной потери актуальности.
staleTime: 1000 * 60 * 60
Используется для:
staleTime: Infinity
Данные никогда не считаются stale автоматически.
Обновление возможно только:
Пример:
useQuery({
queryKey: ['countries'],
queryFn: fetchCountries,
staleTime: Infinity
});
В современных версиях TanStack Query используется
gcTime.
Ранее использовался параметр cacheTime.
useQuery({
queryKey: ['posts'],
queryFn: fetchPosts,
gcTime: 1000 * 60 * 5
});
gcTime определяет:
сколько времени неиспользуемый запрос хранится в памяти после удаления всех подписчиков.
Последовательность работы:
Отвечает за актуальность данных.
Отвечает за время хранения в памяти.
useQuery({
queryKey: ['profile'],
queryFn: fetchProfile,
staleTime: 1000 * 60,
gcTime: 1000 * 60 * 10
});
Что происходит:
Одной из сильнейших стратегий TanStack Query является фоновое обновление.
Когда stale данные уже есть в кеше:
Это устраняет:
Управляет повторным запросом при mount компонента.
useQuery({
queryKey: ['posts'],
queryFn: fetchPosts,
refetchOnMount: true
});
Варианты:
refetchOnMount: true
refetchOnMount: false
refetchOnMount: 'always'
Refetch только stale данных.
Полное отключение автоматического refetch.
Запрос выполняется всегда.
TanStack Query умеет автоматически обновлять данные при возврате во вкладку.
useQuery({
queryKey: ['notifications'],
queryFn: fetchNotifications,
refetchOnWindowFocus: true
});
Когда пользователь возвращается в браузер:
Особенно полезно для:
refetchOnWindowFocus: false
Подходит для:
Запросы могут автоматически обновляться после восстановления интернета.
useQuery({
queryKey: ['messages'],
queryFn: fetchMessages,
refetchOnReconnect: true
});
TanStack Query поддерживает периодический refetch.
useQuery({
queryKey: ['stats'],
queryFn: fetchStats,
refetchInterval: 5000
});
Запрос будет выполняться каждые 5 секунд.
Polling автоматически останавливается:
Поведение можно изменить.
useQuery({
queryKey: ['stats'],
queryFn: fetchStats,
refetchInterval: 5000,
refetchIntervalInBackground: true
});
Позволяет обновлять данные даже в неактивной вкладке.
При пагинации TanStack Query способен сохранять старые данные во время загрузки новых.
useQuery({
queryKey: ['posts', page],
queryFn: () => fetchPosts(page),
placeholderData: keepPreviousData
});
Без этой стратегии интерфейс:
Возникает мерцание UI.
Старые данные остаются отображёнными до завершения нового запроса.
Это особенно важно для:
Позволяет подставлять временные данные.
useQuery({
queryKey: ['post', id],
queryFn: () => fetchPost(id),
placeholderData: {
title: 'Загрузка...',
body: ''
}
});
Placeholder не попадает в кеш как реальные данные.
initialData работает иначе.
useQuery({
queryKey: ['settings'],
queryFn: fetchSettings,
initialData: defaultSettings
});
Особенности:
Предварительная загрузка позволяет заполнить кеш заранее.
await queryClient.prefetchQuery({
queryKey: ['post', id],
queryFn: () => fetchPost(id)
});
Когда компонент откроется:
Наиболее распространённые сценарии:
Каждая страница получает собственный cache entry.
useQuery({
queryKey: ['posts', page],
queryFn: () => fetchPosts(page)
});
Страница 1:
['posts', 1]
Страница 2:
['posts', 2]
Для бесконечных списков используется отдельная стратегия хранения.
useInfiniteQuery({
queryKey: ['feed'],
queryFn: fetchFeed,
getNextPageParam: lastPage => lastPage.nextCursor
});
Кеш содержит массив страниц:
{
pages: [
[...],
[...],
[...]
]
}
Кеш можно инвалидировать вручную.
queryClient.invalidateQueries({
queryKey: ['posts']
});
После invalidation:
queryClient.invalidateQueries({
queryKey: ['posts']
});
Инвалидирует:
['posts']
['posts', 1]
['posts', 2]
['posts', 'popular']
queryClient.invalidateQueries({
queryKey: ['posts', 1],
exact: true
});
Затрагивается только один ключ.
Иногда выгоднее обновлять кеш напрямую.
queryClient.setQueryData(
['post', id],
old => ({
...old,
likes: old.likes + 1
})
);
Преимущества:
Optimistic Updates временно изменяют кеш до ответа сервера.
useMutation({
mutationFn: updatePost,
onMutate: async updatedPost => {
await queryClient.cancelQueries({
queryKey: ['post', updatedPost.id]
});
const previousPost = queryClient.getQueryData([
'post',
updatedPost.id
]);
queryClient.setQueryData(
['post', updatedPost.id],
old => ({
...old,
...updatedPost
})
);
return { previousPost };
}
});
При ошибке кеш можно откатить.
onError: (error, variables, context) => {
queryClient.setQueryData(
['post', variables.id],
context.previousPost
);
}
Правильная структура query keys критически важна для кеширования.
['data']
Все данные смешиваются в один cache entry.
['posts']
['posts', page]
['post', id]
['post-comments', id]
Преимущества:
TanStack Query не требует нормализованного кеша как Redux Toolkit Query или Apollo.
Обычно используется денормализованный подход:
['posts']
['post', id]
Данные могут дублироваться между запросами.
Преимущества:
TanStack Query автоматически очищает неиспользуемые данные.
Механизм GC предотвращает:
Кеш может сохраняться между перезагрузками страницы.
Для этого используется persistence.
Пример:
import { persistQueryClient } from '@tanstack/react-query-persist-client';
import { createSyncStoragePersister } from '@tanstack/query-sync-storage-persister';
const persister = createSyncStoragePersister({
storage: window.localStorage
});
persistQueryClient({
queryClient,
persister
});
После перезагрузки:
TanStack Query поддерживает offline caching.
Данные могут:
Позволяет управлять поведением сети.
useQuery({
queryKey: ['posts'],
queryFn: fetchPosts,
networkMode: 'offlineFirst'
});
Варианты:
'online'
'always'
'offlineFirst'
staleTime: 0
refetchInterval: 3000
staleTime: 10000
refetchOnWindowFocus: true
staleTime: Infinity
gcTime: Infinity
placeholderData: keepPreviousData
initialData
prefetchQuery
dehydrate
hydrate
Настройки можно определить централизованно.
const queryClient = new QueryClient({
defaultOptions: {
queries: {
staleTime: 1000 * 60,
gcTime: 1000 * 60 * 10,
refetchOnWindowFocus: false,
retry: 2
}
}
});
TanStack Query автоматически повторяет запросы.
useQuery({
queryKey: ['posts'],
queryFn: fetchPosts,
retry: 3
});
retryDelay: attempt =>
Math.min(1000 * 2 ** attempt, 30000)
Используется exponential backoff.
Если несколько компонентов одновременно вызывают одинаковый запрос:
useQuery({
queryKey: ['posts'],
queryFn: fetchPosts
});
TanStack Query:
Это одна из важнейших встроенных стратегий оптимизации.
TanStack Query оптимизирует обновления через structural sharing.
Если часть объекта не изменилась:
Компоненты могут подписываться только на часть данных.
useQuery({
queryKey: ['user'],
queryFn: fetchUser,
select: data => data.profile
});
Это снижает количество лишних обновлений UI.
На практике используется комбинация нескольких подходов:
useQuery({
queryKey: ['dashboard'],
queryFn: fetchDashboard,
staleTime: 30000,
gcTime: 1000 * 60 * 10,
refetchOnWindowFocus: true,
refetchInterval: 60000
});
Подобная конфигурация: