В основе работы TanStack Query лежит queryKey —
структурированный идентификатор запроса. Именно он используется для
группировки, поиска и управления кешированными данными.
queryKey может быть как примитивом, так и массивом:
useQuery({
queryKey: ['users'],
queryFn: fetchUsers,
})
useQuery({
queryKey: ['users', 1],
queryFn: () => fetchUserById(1),
})
useQuery({
queryKey: ['users', 1, 'posts'],
queryFn: () => fetchUserPosts(1),
})
Такая структура превращает ключи в иерархическое пространство, где каждый сегмент имеет значение. Это позволяет выполнять выборку не только по точному совпадению, но и по шаблону.
Query filters — это объект, который описывает условия поиска запросов внутри QueryClient. Они используются во всех методах, работающих с множеством запросов:
queryClient.getQueriesDataqueryClient.setQueriesDataqueryClient.invalidateQueriesqueryClient.removeQueriesqueryClient.refetchQueriesБазовая структура фильтра:
const filter = {
queryKey: [],
exact: false,
type: 'active' | 'inactive' | 'all',
stale: boolean,
fetchStatus: 'fetching' | 'paused' | 'idle',
}
На практике чаще всего используются только queryKey и
exact.
Ключевой механизм pattern matching — параметр exact.
queryClient.invalidateQueries({
queryKey: ['users'],
exact: false,
})
Только полное совпадение ключа:
queryKey: ['users'] // совпадает
queryKey: ['users', 1] // не совпадает
Работает как префиксный матчинг:
queryKey: ['users'] совпадает с:
['users']
['users', 1]
['users', 1, 'posts']
Это базовый механизм иерархической фильтрации.
TanStack Query рассматривает queryKey как дерево, где
массив — путь:
['users']
├── ['users', 1]
│ ├── ['users', 1, 'posts']
│ └── ['users', 1, 'followers']
└── ['users', 2]
Запрос:
queryClient.invalidateQueries({
queryKey: ['users', 1],
})
затронет:
но не затронет:
Pattern matching работает не только на первом уровне, но и на каждом элементе массива.
queryClient.invalidateQueries({
queryKey: ['users', 1, 'posts'],
})
Это уже более узкий фильтр, который:
['users', 1, 'followers']Механизм сравнения идёт поэлементно:
users1postsЛюбое расхождение прерывает матчинг, если
exact: true.
queryClient.invalidateQueries({
queryKey: ['users'],
})
Используется после мутаций:
await updateUser(data)
queryClient.invalidateQueries({
queryKey: ['users'],
})
Это гарантирует синхронизацию всего пользовательского кэша.
queryClient.invalidateQueries({
queryKey: ['users', userId],
})
Это ограничивает влияние:
queryClient.removeQueries({
queryKey: ['users', userId],
})
Удаляет данные из кеша полностью, включая состояние и метаданные.
Помимо queryKey, TanStack Query поддерживает
функциональные фильтры через predicate.
queryClient.invalidateQueries({
predicate: (query) => {
return query.queryKey[0] === 'users'
},
})
Это расширяет pattern matching до произвольной логики.
queryClient.invalidateQueries({
queryKey: ['users'],
})
Подходит для:
queryClient.invalidateQueries({
predicate: (query) =>
query.queryKey.includes('users') &&
query.state.dataUpdatedAt < Date.now() - 1000 * 60 * 5,
})
Подходит для:
Query filters могут комбинироваться с состоянием запроса:
queryClient.invalidateQueries({
queryKey: ['users'],
type: 'active',
})
active — только используемые в UI запросыinactive — неиспользуемыеall — всеqueryClient.refetchQueries({
stale: true,
})
Позволяет работать только с устаревшими данными.
queryClient.refetchQueries({
fetchStatus: 'idle',
})
Фильтрует запросы по состоянию выполнения:
fetchingpausedidleВсе параметры фильтра работают совместно:
queryClient.invalidateQueries({
queryKey: ['users'],
type: 'active',
stale: true,
})
Логика:
Фильтры используются не только для инвалидирования, но и для прямого изменения кеша:
queryClient.setQueriesData(
{
queryKey: ['users'],
},
(oldData) => {
return oldData?.map(user =>
user.id === 1 ? { ...user, name: 'Upd ated' } : user
)
}
)
Это позволяет массово обновлять данные без повторного запроса.
Структура ключей может быть глубокой:
['org', orgId, 'users', userId, 'posts', postId]
Фильтр:
queryClient.invalidateQueries({
queryKey: ['org', orgId, 'users'],
})
Затронет:
Но не затронет:
Чем более точный queryKey, тем меньше лишних перезапросов:
queryClient.invalidateQueries({
queryKey: ['users', userId, 'profile'],
exact: true,
})
Такой подход уменьшает:
queryKey: ['data']
Проблема:
predicate: (q) => q.queryKey.toString().includes('user')
Проблема:
['a', 'b', 'c', 'd', 'e', 'f']
Проблема:
queryKey следует рассматривать как адрес:
resource → entity → subresource → detail
Примеры:
['users']
['users', userId]
['users', userId, 'posts']
['users', userId, 'posts', postId]
Фильтры работают как маршрутизатор по этому дереву, где каждый уровень добавляет ограничение области действия.
Query filters напрямую воздействуют на внутренний реестр QueryClient:
Каждый вызов:
queryClient.invalidateQueries(...)
фактически проходит через:
Если фильтр не находит совпадений:
Это делает систему безопасной для массовых операций.
Динамические ключи:
queryKey: ['users', userId, tab]
Фильтр:
queryClient.invalidateQueries({
queryKey: ['users', userId],
})
Работает корректно независимо от tab, так как сравнение
идёт по префиксу.
Порядок элементов строго значим:
['users', 1, 'posts'] !== ['posts', 1, 'users']
Это означает, что pattern matching не является наборным (se t-based), а позиционным.
В реальных архитектурах чаще всего используется комбинация:
Типовая схема:
queryClient.invalidateQueries({
queryKey: ['users', userId],
})
и
queryClient.invalidateQueries({
queryKey: ['users'],
type: 'active',
})
разделяют ответственность между точечными и массовыми обновлениями кеша.