Организация query keys

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']
})

Инвалидация затронет все запросы:

  • [‘posts’]
  • [‘posts’, ‘list’]
  • [‘posts’, ‘list’, { page: 1 }]
  • [‘posts’, 10]

Стабильность query key

Query key должен быть стабильным между рендерами. Любое нестабильное значение приводит к созданию нового запроса и потере кэша.

Проблемный пример:

useQuery({
  queryKey: ['users', { page: Math.random() }],
  queryFn: fetchUsers
})

Каждый рендер создаёт новый ключ, что полностью ломает кэширование.

Правильный подход:

useQuery({
  queryKey: ['users', { page }],
  queryFn: () => fetchUsers(page)
})

Объекты внутри query key

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 })
})

Такой подход делает кэш предсказуемым: каждая комбинация фильтров хранится отдельно.

Query key фабрики

В крупных приложениях ручное создание ключей приводит к дублированию и ошибкам. Решением становится использование фабрик.

Пример:

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)
})

Преимущества:

  • единая точка определения структуры ключей
  • отсутствие дублирования строк
  • упрощённая инвалидация
  • предсказуемая масштабируемость

Типизация query keys

В TypeScript query keys могут быть строго типизированы для предотвращения ошибок:

type UserQueryKey =
  | ['users']
  | ['users', 'list', { page: number }]
  | ['users', 'detail', number]

Это ограничивает возможность случайно создать некорректный ключ и повышает надёжность кэш-структуры.

Связь query key и invalidateQueries

Инвалидация работает строго через сопоставление ключей:

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]

Ошибки организации query keys

Частые проблемы:

1. Потеря параметров

['users']

без учёта фильтров приводит к конфликту данных.

2. Нестабильные объекты

['users', { page: 1 }]

создаваемые inline без контроля могут приводить к лишним ререндер-запросам при сложных структурах.

3. Смешивание доменов

['data', 'users', 'posts']

без чёткой иерархии усложняет инвалидацию.

4. Разные форматы ключей

'users'
['users']

смешивание строк и массивов приводит к фрагментации кэша.

Практика масштабируемой структуры

В крупных системах эффективна модель доменной сегментации:

['auth', 'session']
['auth', 'user']
['catalog', 'products']
['catalog', 'categories']
['orders', 'list']
['orders', 'detail']

Такая структура позволяет:

  • изолировать домены
  • управлять кэшем точечно
  • минимизировать побочные эффекты инвалидации

Итоговая модель поведения query keys

Query key выполняет одновременно три функции:

  • идентификация запроса
  • ключ к кэшу
  • инструмент группировки и инвалидации

Его структура напрямую определяет архитектуру работы с серверным состоянием в приложении и влияет на предсказуемость всей системы данных.