Query keys в TanStack Query являются фундаментальной частью всей системы кэширования и идентификации данных. От того, как они структурированы, зависит предсказуемость обновлений, корректность инвалидации, производительность повторных запросов и удобство масштабирования приложения.
Query key — это идентификатор запроса, по которому TanStack Query сохраняет, находит и управляет кэшированными данными. В простейшем случае это строка:
useQuery({
queryKey: ['users'],
queryFn: fetchUsers
})
Однако даже в таком виде query key уже является массивом. Это не случайность: массив обеспечивает расширяемость и строгую структуру идентификации.
Ключевой принцип: query key должен однозначно описывать данные, которые возвращает queryFn.
TanStack Query использует массив как базовый формат:
['users']
['users', 'list']
['users', userId]
['users', userId, 'posts']
Такой подход позволяет строить иерархическую модель данных, где каждый элемент массива уточняет контекст запроса.
Пример:
useQuery({
queryKey: ['users', 42],
queryFn: () => fetchUser(42)
})
Здесь 42 становится частью идентификатора, и данные
пользователя с id 42 никогда не будут смешаны с другими
пользователями.
Массив query key работает как путь:
Пример:
['posts', 'list', { page: 1, limit: 10 }]
Такая структура позволяет группировать запросы по общему префиксу
['posts'].
Это критично для инвалидации:
queryClient.invalidateQueries({
queryKey: ['posts']
})
Инвалидация затронет все запросы:
Query key должен быть стабильным между рендерами. Любое нестабильное значение приводит к созданию нового запроса и потере кэша.
Проблемный пример:
useQuery({
queryKey: ['users', { page: Math.random() }],
queryFn: fetchUsers
})
Каждый рендер создаёт новый ключ, что полностью ломает кэширование.
Правильный подход:
useQuery({
queryKey: ['users', { page }],
queryFn: () => fetchUsers(page)
})
TanStack Query допускает использование объектов внутри массива, но с важным условием: объект должен быть сериализуемым и стабильным.
['users', { page: 1, filter: 'active' }]
Однако порядок полей в объекте имеет значение при сравнении ключей.
Эта особенность приводит к необходимости контролировать создание объектов:
const queryKey = ['users', { page, filter }]
или заранее стабилизировать объект:
const filters = useMemo(() => ({ page, filter }), [page, filter])
TanStack Query не сравнивает query keys поверхностно или по ссылке. Используется глубокое сравнение структуры массива.
Это означает:
['users', 1] !== ['users', 1]
с точки зрения ссылок, но равны с точки зрения TanStack Query.
Таким образом, допустимо создавать ключи inline, если они структурно одинаковы.
Одна из сильных сторон query keys — возможность группировать запросы через общие префиксы.
['users']
['users', 'detail']
['users', 'list']
['users', 'search']
Это позволяет выполнять операции над целыми группами:
queryClient.invalidateQueries({
queryKey: ['users']
})
или более точечно:
queryClient.invalidateQueries({
queryKey: ['users', 'list']
})
Чем длиннее ключ, тем более специфична область действия.
Любые параметры запроса должны быть включены в query key. Это обеспечивает корректное кэширование разных вариаций одного запроса.
Пример пагинации:
useQuery({
queryKey: ['products', 'list', page, limit],
queryFn: () => fetchProducts(page, limit)
})
Если параметры не включены, кэш будет перезаписываться, и разные страницы начнут конфликтовать.
Фильтры и сортировка также являются частью идентичности данных:
useQuery({
queryKey: ['products', 'list', { category, sort, order }],
queryFn: () => fetchProducts({ category, sort, order })
})
Такой подход делает кэш предсказуемым: каждая комбинация фильтров хранится отдельно.
В крупных приложениях ручное создание ключей приводит к дублированию и ошибкам. Решением становится использование фабрик.
Пример:
const userKeys = {
all: ['users'],
lists: () => [...userKeys.all, 'list'],
list: (filters) => [...userKeys.lists(), filters],
details: () => [...userKeys.all, 'detail'],
detail: (id) => [...userKeys.details(), id]
}
Использование:
useQuery({
queryKey: userKeys.detail(42),
queryFn: () => fetchUser(42)
})
Преимущества:
В TypeScript query keys могут быть строго типизированы для предотвращения ошибок:
type UserQueryKey =
| ['users']
| ['users', 'list', { page: number }]
| ['users', 'detail', number]
Это ограничивает возможность случайно создать некорректный ключ и повышает надёжность кэш-структуры.
Инвалидация работает строго через сопоставление ключей:
queryClient.invalidateQueries({
queryKey: ['users']
})
Это означает, что неправильная структура ключа приводит к тому, что данные не обновляются.
Пример ошибки:
queryKey: ['user'] // вместо ['users']
В этом случае инвалидация по [‘users’] не затронет [‘user’].
TanStack Query поддерживает частичное совпадение через префиксы.
queryClient.invalidateQueries({
queryKey: ['users'],
exact: false
})
Это поведение позволяет управлять целыми сегментами кэша без перечисления всех возможных вариаций.
В сложных приложениях важно поддерживать единый стиль:
Пример стандарта:
['domain', 'entity', 'operation', params]
или более прикладной вариант:
['orders', 'list', { status, page }]
['orders', 'detail', orderId]
Частые проблемы:
['users']
без учёта фильтров приводит к конфликту данных.
['users', { page: 1 }]
создаваемые inline без контроля могут приводить к лишним ререндер-запросам при сложных структурах.
['data', 'users', 'posts']
без чёткой иерархии усложняет инвалидацию.
'users'
['users']
смешивание строк и массивов приводит к фрагментации кэша.
В крупных системах эффективна модель доменной сегментации:
['auth', 'session']
['auth', 'user']
['catalog', 'products']
['catalog', 'categories']
['orders', 'list']
['orders', 'detail']
Такая структура позволяет:
Query key выполняет одновременно три функции:
Его структура напрямую определяет архитектуру работы с серверным состоянием в приложении и влияет на предсказуемость всей системы данных.