Логирование кеша в библиотеке TanStack Query представляет собой процесс отслеживания всех операций, связанных с хранением, обновлением, инвалидированием и удалением данных из клиентского кеша. В сложных приложениях кеш становится полноценным слоем управления состоянием, поэтому без наблюдаемости невозможно эффективно диагностировать проблемы производительности, гонки запросов, лишние перерендеры и ошибки синхронизации данных.
Логирование позволяет:
Основой кеширования является объект QueryClient, внутри
которого располагаются:
QueryCacheMutationCacheКаждый query хранит:
Упрощённая схема:
const queryClient = new QueryClient({
queryCache: new QueryCache(),
mutationCache: new MutationCache()
});
Логирование обычно строится вокруг:
QueryCache;MutationCache;TanStack Query поддерживает кастомный logger.
Пример настройки:
import { QueryClient } from '@tanstack/react-query';
const queryClient = new QueryClient({
logger: {
log: (...args) => {
console.log('[LOG]', ...args);
},
warn: (...args) => {
console.warn('[WARN]', ...args);
},
error: (...args) => {
console.error('[ERROR]', ...args);
}
}
});
Logger применяется библиотекой для:
Главный механизм логирования кеша — подписка на изменения
QueryCache.
import {
QueryClient,
QueryCache
} from '@tanstack/react-query';
const queryCache = new QueryCache();
queryCache.subscribe((event) => {
console.log(event);
});
const queryClient = new QueryClient({
queryCache
});
Событие содержит:
{
type,
query
}
Типы событий:
addedremovedupdatedobserverAddedobserverRemovedobserverResultsUpdatedobserverOptionsUpdatedqueryCache.subscribe((event) => {
if (event.type === 'added') {
console.log('Query создан');
console.log({
queryKey: event.query.queryKey,
state: event.query.state
});
}
});
Результат:
{
queryKey: ['posts'],
state: {
status: 'pending',
fetchStatus: 'fetching'
}
}
Такое логирование помогает анализировать:
Удаление query особенно важно при диагностике очистки памяти.
queryCache.subscribe((event) => {
if (event.type === 'removed') {
console.log('Query удалён');
console.log(event.query.queryKey);
}
});
Это позволяет:
cacheTime;Самый полезный тип событий — updated.
queryCache.subscribe((event) => {
if (event.type === 'updated') {
const query = event.query;
console.log({
queryKey: query.queryKey,
status: query.state.status,
fetchStatus: query.state.fetchStatus,
dataUpdatedAt: query.state.dataUpdatedAt,
errorUpdatedAt: query.state.errorUpdatedAt
});
}
});
Это позволяет видеть:
query.state.status
Возможные значения:
pendingsuccesserrorquery.state.fetchStatus
Возможные значения:
idlefetchingpausedОпределение stale-состояния критично для анализа повторных запросов.
queryCache.subscribe((event) => {
if (event.type !== 'updated') {
return;
}
const query = event.query;
console.log({
key: query.queryKey,
isStale: query.isStale()
});
});
Проблемы stale-логики:
staleTime;Инвалидирование часто становится источником неожиданных refetch-операций.
const invalidate = async () => {
console.log('Инвалидирование posts');
await queryClient.invalidateQueries({
queryKey: ['posts']
});
};
Для более детального контроля:
queryCache.subscribe((event) => {
if (event.type !== 'updated') {
return;
}
const query = event.query;
if (query.state.isInvalidated) {
console.log('Query инвалидирован');
console.log(query.queryKey);
}
});
Иногда требуется анализировать содержимое кеша.
const query = queryClient.getQueryCache().find({
queryKey: ['posts']
});
console.log(query.state.data);
Для сериализации:
console.log(
JSON.stringify(query.state.data, null, 2)
);
Полезно при диагностике крупных приложений.
const queries = queryClient
.getQueryCache()
.getAll();
console.log(
queries.map((query) => ({
key: query.queryKey,
status: query.state.status,
observers: query.getObserversCount()
}))
);
Такой снимок помогает:
TanStack Query хранит временные метки.
const query = queryClient
.getQueryCache()
.find({
queryKey: ['posts']
});
console.log({
dataUpdatedAt: query.state.dataUpdatedAt,
errorUpdatedAt: query.state.errorUpdatedAt
});
На основе этих данных можно:
Каждый компонент создаёт observer.
queryCache.subscribe((event) => {
if (event.type === 'observerAdded') {
console.log('Подписчик добавлен');
console.log(event.query.queryKey);
}
if (event.type === 'observerRemoved') {
console.log('Подписчик удалён');
console.log(event.query.queryKey);
}
});
Это помогает выявлять:
const query = queryClient
.getQueryCache()
.find({
queryKey: ['posts']
});
console.log(
query.getObserversCount()
);
Большое число observers может означать:
Часто логирование кеша комбинируется с логированием сети.
const fetchPosts = async () => {
console.log('HTTP запрос /posts');
const response = await fetch('/api/posts');
return response.json();
};
Комбинация сетевых и кеш-событий позволяет видеть:
useQuery({
queryKey: ['posts'],
queryFn: fetchPosts,
retry: 3,
retryDelay: 1000
});
Через logger:
const queryClient = new QueryClient({
logger: {
error: (...args) => {
console.error('Retry ошибка', args);
},
log: console.log,
warn: console.warn
}
});
queryCache.subscribe((event) => {
if (event.type !== 'updated') {
return;
}
const query = event.query;
if (query.state.status === 'error') {
console.error({
key: query.queryKey,
error: query.state.error
});
}
});
Можно дополнительно логировать:
Для mutation используется MutationCache.
import {
MutationCache
} from '@tanstack/react-query';
const mutationCache = new MutationCache({
onError(error) {
console.error(error);
},
onSuccess(data) {
console.log(data);
}
});
Подключение:
const queryClient = new QueryClient({
mutationCache
});
mutationCache.subscribe((event) => {
console.log(event);
});
События mutation:
addedremovedupdatedobserverAddedobserverRemovedOptimistic update часто становится источником сложных ошибок.
useMutation({
mutationFn: updatePost,
onMutate: async (newPost) => {
console.log('Optimistic update');
const previous =
queryClient.getQueryData(['posts']);
queryClient.setQueryData(
['posts'],
(old) => [...old, newPost]
);
return { previous };
},
onError: (error, variables, context) => {
console.log('Rollback');
queryClient.setQueryData(
['posts'],
context.previous
);
}
});
Для глубокого анализа можно сравнивать старые и новые данные.
queryCache.subscribe((event) => {
if (event.type !== 'updated') {
return;
}
const query = event.query;
console.log({
oldData: query.state.dataUpdateCount - 1,
updates: query.state.dataUpdateCount
});
});
const logQuery = (queryFn) => {
return async (...args) => {
const start = performance.now();
try {
const result = await queryFn(...args);
console.log({
duration: performance.now() - start,
success: true
});
return result;
} catch (error) {
console.error({
duration: performance.now() - start,
success: false,
error
});
throw error;
}
};
};
Использование:
useQuery({
queryKey: ['posts'],
queryFn: logQuery(fetchPosts)
});
const timedFetch = async () => {
const started = Date.now();
const response = await fetch('/api/posts');
const data = await response.json();
console.log({
duration: Date.now() - started
});
return data;
};
Такие метрики помогают:
Популярный сценарий — отправка ошибок в Sentry.
import * as Sentry from '@sentry/react';
const queryClient = new QueryClient({
logger: {
log: console.log,
warn: console.warn,
error: (error) => {
Sentry.captureException(error);
}
}
});
Пример отправки логов:
const sendMetric = (payload) => {
fetch('/metrics', {
method: 'POST',
body: JSON.stringify(payload)
});
};
queryCache.subscribe((event) => {
sendMetric({
type: event.type,
timestamp: Date.now()
});
});
Официальные devtools позволяют анализировать:
Подключение:
import {
ReactQueryDevtools
} from '@tanstack/react-query-devtools';
<ReactQueryDevtools initialIsOpen={false} />
queryClient.clear();
console.log('Кеш очищен');
Для частичной очистки:
queryClient.removeQueries({
queryKey: ['posts']
});
При SSR важно отслеживать перенос кеша.
import {
dehydrate
} from '@tanstack/react-query';
const dehydratedState =
dehydrate(queryClient);
console.log(dehydratedState);
Hydration:
import {
Hydrate
} from '@tanstack/react-query';
<Hydrate state={pageProps.dehydratedState}>
<App />
</Hydrate>
При использовании persistent cache:
persistQueryClient({
queryClient,
persister
});
Можно логировать:
Признаки проблем:
cacheTime слишком большой.Диагностика:
setInterval(() => {
const queries =
queryClient
.getQueryCache()
.getAll();
console.log({
count: queries.length
});
}, 5000);
Чрезмерное логирование может:
Плохой пример:
queryCache.subscribe((event) => {
console.log(event);
});
Лучше:
queryCache.subscribe((event) => {
if (event.type !== 'updated') {
return;
}
if (event.query.queryKey[0] !== 'posts') {
return;
}
console.log(event.query.state.status);
});
const ignoredQueries = [
'notifications',
'metrics'
];
queryCache.subscribe((event) => {
const key = event.query.queryKey[0];
if (ignoredQueries.includes(key)) {
return;
}
console.log(event);
});
Хорошей практикой считается JSON-формат.
console.log(
JSON.stringify({
timestamp: Date.now(),
type: event.type,
queryKey: event.query.queryKey,
status: event.query.state.status
})
);
Преимущества:
class QueryLogger {
log(event) {
console.log({
type: event.type,
key: event.query.queryKey
});
}
error(error) {
console.error(error);
}
}
const logger = new QueryLogger();
queryCache.subscribe((event) => {
logger.log(event);
});
Такой подход упрощает: