Механизм query invalidation используется для
принудительного помечания кешированных запросов как устаревших. После
инвалидирования TanStack Query инициирует повторную загрузку данных или
откладывает её до следующего момента обращения к запросу — в зависимости
от конфигурации.
Инвалидация является центральным механизмом синхронизации клиентского кеша с серверным состоянием. Вместо ручного обновления всех структур данных библиотека позволяет пометить определённые запросы как неактуальные и автоматически перезапросить данные.
Базовый пример:
queryClient.invalidateQueries({
queryKey: ['posts']
});
После вызова:
['posts']
все запросы с таким ключом переходят в состояние stale.
Серверное состояние постоянно изменяется:
Без invalidation UI быстро начинает отображать устаревшие данные.
Пример:
useQuery({
queryKey: ['users'],
queryFn: fetchUsers
});
Если пользователь создал нового пользователя через mutation:
await createUser(data);
список users останется старым, пока не произойдёт
refetch.
Именно для этого используется invalidation.
В TanStack Query invalidation почти всегда инициируется событием.
Типичные источники событий:
| Событие | Пример |
|---|---|
| Mutation | Создание записи |
| WebSocket | Новый комментарий |
| SSE | Серверное уведомление |
| Таймер | Автообновление |
| Focus event | Возврат на вкладку |
| Reconnect | Восстановление сети |
| Broadcast | Изменение в другой вкладке |
| Custom event | Пользовательское действие |
| Route change | Переход между страницами |
Наиболее распространённый сценарий.
const queryClient = useQueryClient();
const mutation = useMutation({
mutationFn: createPost,
onSuccess: () => {
queryClient.invalidateQueries({
queryKey: ['posts']
});
}
});
После успешного создания записи:
posts помечается stale;Многие разработчики делают так:
const { refetch } = useQuery(...);
await mutation.mutateAsync(data);
refetch();
Подход имеет недостатки:
invalidateQueries работает на уровне глобального
кеша.
TanStack Query поддерживает частичное совпадение ключей.
Пример структуры:
['posts']
['posts', 1]
['posts', 2]
['posts', 'comments']
Инвалидация:
queryClient.invalidateQueries({
queryKey: ['posts']
});
затронет все перечисленные запросы.
Для ограничения области invalidation используется
exact.
queryClient.invalidateQueries({
queryKey: ['posts'],
exact: true
});
Теперь будет инвалидирован только:
['posts']
но не:
['posts', 1]
const mutation = useMutation({
mutationFn: updateUser,
onSuccess: (updatedUser) => {
queryClient.invalidateQueries({
queryKey: ['users']
});
queryClient.invalidateQueries({
queryKey: ['user', updatedUser.id]
});
}
});
Инвалидируются:
Иногда одно действие влияет на несколько областей приложения.
Пример интернет-магазина:
const mutation = useMutation({
mutationFn: createOrder,
onSuccess: () => {
queryClient.invalidateQueries({
queryKey: ['cart']
});
queryClient.invalidateQueries({
queryKey: ['orders']
});
queryClient.invalidateQueries({
queryKey: ['inventory']
});
queryClient.invalidateQueries({
queryKey: ['profile']
});
}
});
Создание заказа влияет сразу на:
При большом проекте invalidation начинает дублироваться.
Плохой пример:
onSuccess: () => {
queryClient.invalidateQueries({ queryKey: ['posts'] });
queryClient.invalidateQueries({ queryKey: ['feed'] });
queryClient.invalidateQueries({ queryKey: ['stats'] });
}
Лучше выносить в отдельные функции.
export const invalidatePostQueries = async (queryClient) => {
await Promise.all([
queryClient.invalidateQueries({
queryKey: ['posts']
}),
queryClient.invalidateQueries({
queryKey: ['feed']
}),
queryClient.invalidateQueries({
queryKey: ['stats']
})
]);
};
Использование:
onSuccess: async () => {
await invalidatePostQueries(queryClient);
}
export const createMutation = ({
mutationFn,
invalidate = []
}) => {
const queryClient = useQueryClient();
return useMutation({
mutationFn,
onSuccess: async () => {
await Promise.all(
invalidate.map((key) =>
queryClient.invalidateQueries({
queryKey: key
})
)
);
}
});
};
Использование:
const mutation = createMutation({
mutationFn: createPost,
invalidate: [
['posts'],
['feed']
]
});
Сервер может отправлять уведомления:
{
"type": "NEW_MESSAGE",
"chatId": 15
}
Обработка:
socket.onmess age = (event) => {
const data = JSON.parse(event.data);
if (data.type === 'NEW_MESSAGE') {
queryClient.invalidateQueries({
queryKey: ['chat', data.chatId]
});
}
};
Realtime-системы активно используют invalidation:
Главная идея:
Существует два подхода обновления кеша.
queryClient.invalidateQueries({
queryKey: ['posts']
});
queryClient.setQueryData(
['posts'],
(old) => [...old, newPost]
);
invalidateQueries предпочтителен:
setQueryData полезен:
Часто используется одновременно:
onSuccess: (newPost) => {
queryClient.setQueryData(
['post', newPost.id],
newPost
);
queryClient.invalidateQueries({
queryKey: ['posts']
});
}
TanStack Query умеет автоматически рефетчить данные при возвращении на вкладку.
const queryClient = new QueryClient({
defaultOptions: {
queries: {
refetchOnWindowFocus: true
}
}
});
Механизм основан на invalidation stale-запросов.
const queryClient = new QueryClient({
defaultOptions: {
queries: {
refetchOnReconnect: true
}
}
});
После reconnect запросы обновляются автоматически.
eventBus.on('USER_UPDATED', (userId) => {
queryClient.invalidateQueries({
queryKey: ['user', userId]
});
});
window.addEventListener('profile-updated', () => {
queryClient.invalidateQueries({
queryKey: ['profile']
});
});
setInterval(() => {
queryClient.invalidateQueries({
queryKey: ['notifications']
});
}, 30000);
Немедленно выполняет запрос.
refetch();
Помечает запрос устаревшим.
queryClient.invalidateQueries(...)
Refetch может быть выполнен позже.
TanStack Query поддерживает гибкую фильтрацию.
queryClient.invalidateQueries({
predicate: (query) => {
return query.queryKey[0] === 'posts';
}
});
queryClient.invalidateQueries({
queryKey: ['posts'],
refetchType: 'active'
});
Варианты:
| Значение | Описание |
|---|---|
| active | Только активные |
| inactive | Только неактивные |
| all | Все |
Иногда требуется обновить весь кеш.
queryClient.invalidateQueries();
Но такой подход:
Качество invalidation напрямую зависит от структуры query keys.
Плохая структура:
['data']
Невозможно гибко инвалидировать данные.
Хорошая структура:
['users']
['users', userId]
['users', userId, 'posts']
['posts']
['posts', postId]
Крупные приложения часто строят вокруг событийной архитектуры.
eventBus.emit('POST_CREATED', post);
Отдельный слой подписывается:
eventBus.on('POST_CREATED', () => {
queryClient.invalidateQueries({
queryKey: ['posts']
});
queryClient.invalidateQueries({
queryKey: ['feed']
});
});
Mutation ничего не знает о кешах.
Это уменьшает связность системы.
При микрофронтенд-архитектуре события могут приходить из разных приложений.
window.dispatchEvent(
new CustomEvent('cart:updated')
);
Другой frontend:
window.addEventListener('cart:updated', () => {
queryClient.invalidateQueries({
queryKey: ['cart']
});
});
Для синхронизации вкладок используется BroadcastChannel.
const channel = new BroadcastChannel('app');
channel.onmess age = (event) => {
if (event.data.type === 'INVALIDATE_POSTS') {
queryClient.invalidateQueries({
queryKey: ['posts']
});
}
};
staleTime влияет на поведение invalidation.
useQuery({
queryKey: ['posts'],
queryFn: fetchPosts,
staleTime: 1000 * 60 * 5
});
Даже если staleTime ещё не истёк:
queryClient.invalidateQueries({
queryKey: ['posts']
});
запрос немедленно становится stale.
cacheTime определяет:
Но invalidation не удаляет кеш.
Она только помечает его устаревшим.
const mutation = useMutation({
mutationFn: updatePost,
onMutate: async (updatedPost) => {
await queryClient.cancelQueries({
queryKey: ['posts']
});
const previous =
queryClient.getQueryData(['posts']);
queryClient.setQueryData(
['posts'],
(old) =>
old.map((post) =>
post.id === updatedPost.id
? updatedPost
: post
)
);
return { previous };
},
onError: (error, variables, context) => {
queryClient.setQueryData(
['posts'],
context.previous
);
},
onSettled: () => {
queryClient.invalidateQueries({
queryKey: ['posts']
});
}
});
onSettled выполняется:
Это гарантирует синхронизацию с сервером независимо от результата optimistic update.
Для useInfiniteQuery invalidation работает
аналогично.
queryClient.invalidateQueries({
queryKey: ['feed']
});
Все страницы будут считаться stale.
При SSR invalidation используется осторожно.
Проблемы:
Типичный пример:
const logout = async () => {
await api.logout();
queryClient.clear();
};
Иногда лучше:
queryClient.invalidateQueries();
Но clear() полностью очищает кеш и обычно безопаснее при
смене пользователя.
TanStack Query Devtools позволяют видеть:
Это критически важно для диагностики сложных invalidation-сценариев.
queryClient.invalidateQueries();
после любой mutation.
Результат:
['data']
Невозможно управлять отдельными частями кеша.
Mutation завершилась успешно, но UI остался старым.
await mutation.mutateAsync();
refetch();
queryClient.invalidateQueries(...);
Происходит двойной запрос.
Локальный кеш обновлён, но сервер вернул другое состояние.
Mutation должна сообщать о событии, а не управлять кешем напрямую.
Иерархическая структура:
['users']
['users', id]
['users', id, 'posts']
значительно упрощает invalidation.
Лучше инвалидировать:
['posts']
чем весь кеш.
Это даёт:
Крупные приложения выигрывают от централизованного управления событиями и кешем.